diff options
234 files changed, 5420 insertions, 3574 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 4e34b63be0d6..59a7cbc16587 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -21,6 +21,7 @@ aconfig_declarations_group { // !!! KEEP THIS LIST ALPHABETICAL !!! "aconfig_mediacodec_flags_java_lib", "android.adaptiveauth.flags-aconfig-java", + "android.app.appfunctions.flags-aconfig-java", "android.app.contextualsearch.flags-aconfig-java", "android.app.flags-aconfig-java", "android.app.ondeviceintelligence-aconfig-java", @@ -1383,6 +1384,21 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// AppFunctions +aconfig_declarations { + name: "android.app.appfunctions.flags-aconfig", + exportable: true, + package: "android.app.appfunctions.flags", + container: "system", + srcs: ["core/java/android/app/appfunctions/flags/flags.aconfig"], +} + +java_aconfig_library { + name: "android.app.appfunctions.flags-aconfig-java", + aconfig_declarations: "android.app.appfunctions.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Adaptive Auth aconfig_declarations { name: "android.adaptiveauth.flags-aconfig", diff --git a/apct-tests/perftests/multiuser/Android.bp b/apct-tests/perftests/multiuser/Android.bp index 856dba3f804c..9eea712b33dd 100644 --- a/apct-tests/perftests/multiuser/Android.bp +++ b/apct-tests/perftests/multiuser/Android.bp @@ -45,3 +45,8 @@ filegroup { "trace_configs/trace_config_multi_user.textproto", ], } + +prebuilt_etc { + name: "trace_config_multi_user.textproto", + src: ":multi_user_trace_config", +} diff --git a/core/api/current.txt b/core/api/current.txt index ea039a7103a3..36c9175830a3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10684,6 +10684,7 @@ package android.content { field public static final String ACTIVITY_SERVICE = "activity"; field public static final String ALARM_SERVICE = "alarm"; field public static final String APPWIDGET_SERVICE = "appwidget"; + field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String APP_FUNCTION_SERVICE = "app_function"; field public static final String APP_OPS_SERVICE = "appops"; field public static final String APP_SEARCH_SERVICE = "app_search"; field public static final String AUDIO_SERVICE = "audio"; diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 89efa9b77a60..d31881265064 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -961,6 +961,17 @@ public abstract class ActivityManagerInternal { @Nullable VoiceInteractionManagerProvider provider); /** + * Get whether or not the previous user's packages will be killed before the user is + * stopped during a user switch. + * + * <p> The primary use case of this method is for {@link com.android.server.SystemService} + * classes to call this API in their + * {@link com.android.server.SystemService#onUserSwitching} method implementation to prevent + * restarting any of the previous user's processes that will be killed during the user switch. + */ + public abstract boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId); + + /** * Sets whether the current foreground user (and its profiles) should be stopped after switched * out. */ diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index e73f4718732f..114a2c4d5649 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -16,6 +16,8 @@ package android.app; +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; + import android.accounts.AccountManager; import android.accounts.IAccountManager; import android.adservices.AdServicesFrameworkInitializer; @@ -28,6 +30,8 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.app.ambientcontext.AmbientContextManager; import android.app.ambientcontext.IAmbientContextManager; +import android.app.appfunctions.AppFunctionManager; +import android.app.appfunctions.IAppFunctionManager; import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; import android.app.contentsuggestions.ContentSuggestionsManager; @@ -925,6 +929,21 @@ public final class SystemServiceRegistry { return new CompanionDeviceManager(service, ctx.getOuterContext()); }}); + if (enableAppFunctionManager()) { + registerService(Context.APP_FUNCTION_SERVICE, AppFunctionManager.class, + new CachedServiceFetcher<>() { + @Override + public AppFunctionManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IAppFunctionManager service; + //TODO(b/357551503): If the feature not present avoid look up every time + service = IAppFunctionManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.APP_FUNCTION_SERVICE)); + return new AppFunctionManager(service, ctx.getOuterContext()); + } + }); + } + registerService(Context.VIRTUAL_DEVICE_SERVICE, VirtualDeviceManager.class, new CachedServiceFetcher<VirtualDeviceManager>() { @Override diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java new file mode 100644 index 000000000000..a01e373c5e83 --- /dev/null +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.appfunctions; + +import android.annotation.SystemService; +import android.content.Context; + +/** + * Provides app functions related functionalities. + * + * <p>App function is a specific piece of functionality that an app offers to the system. These + * functionalities can be integrated into various system features. + * + * @hide + */ +@SystemService(Context.APP_FUNCTION_SERVICE) +public final class AppFunctionManager { + private final IAppFunctionManager mService; + private final Context mContext; + + /** + * TODO(b/357551503): add comments when implement this class + * + * @hide + */ + public AppFunctionManager(IAppFunctionManager mService, Context context) { + this.mService = mService; + this.mContext = context; + } +} diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl new file mode 100644 index 000000000000..018bc758f69f --- /dev/null +++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.appfunctions; + +/** +* Interface between an app and the server implementation service (AppFunctionManagerService). +* @hide +*/ +oneway interface IAppFunctionManager { +}
\ No newline at end of file diff --git a/core/java/android/app/appfunctions/flags/flags.aconfig b/core/java/android/app/appfunctions/flags/flags.aconfig new file mode 100644 index 000000000000..367effc9e9bb --- /dev/null +++ b/core/java/android/app/appfunctions/flags/flags.aconfig @@ -0,0 +1,11 @@ +package: "android.app.appfunctions.flags" +container: "system" + +flag { + name: "enable_app_function_manager" + is_exported: true + is_fixed_read_only: true + namespace: "machine_learning" + description: "This flag the new App Function manager system service." + bug: "357551503" +}
\ No newline at end of file diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 9aebfc8e5fd7..9dccc9ae7145 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,6 +16,7 @@ package android.content; +import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import android.annotation.AttrRes; @@ -51,6 +52,7 @@ import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.VrManager; import android.app.ambientcontext.AmbientContextManager; +import android.app.appfunctions.AppFunctionManager; import android.app.people.PeopleManager; import android.app.time.TimeManager; import android.companion.virtual.VirtualDeviceManager; @@ -6310,6 +6312,16 @@ public abstract class Context { /** * Use with {@link #getSystemService(String)} to retrieve an + * {@link AppFunctionManager} for + * executing app functions. + * + * @see #getSystemService(String) + */ + @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) + public static final String APP_FUNCTION_SERVICE = "app_function"; + + /** + * Use with {@link #getSystemService(String)} to retrieve an * {@link android.content.integrity.AppIntegrityManager}. * @hide */ diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING index b0ab11f48858..1fab3cffcebd 100644 --- a/core/java/android/content/pm/TEST_MAPPING +++ b/core/java/android/content/pm/TEST_MAPPING @@ -171,6 +171,17 @@ "include-filter": "android.content.pm.cts.PackageManagerShellCommandMultiUserTest" } ] + }, + { + "name":"CtsPackageInstallerCUJTestCases", + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + } + ] } ] } diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 59fca3bc8cb1..273519b75f93 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -288,6 +288,13 @@ flag { } flag { + name: "stop_previous_user_apps" + namespace: "multiuser" + description: "Stop the previous user apps early in a user switch" + bug: "323200731" +} + +flag { name: "disable_private_space_items_on_home" namespace: "profile_experiences" description: "Disables adding items belonging to Private Space on Home Screen manually as well as automatically" diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 9007b62bccfc..b11961cc2b21 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -638,17 +638,17 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Set caller's component name for getting logo icon/description. This should only be used * by ConfirmDeviceCredentialActivity, see b/337082634 for more context. * - * @param componentNameForConfirmDeviceCredentialActivity set the component name for - * ConfirmDeviceCredentialActivity. + * @param realCaller set the component name of real caller for + * ConfirmDeviceCredentialActivity. * @return This builder. * @hide */ @NonNull @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL}) - public Builder setComponentNameForConfirmDeviceCredentialActivity( - ComponentName componentNameForConfirmDeviceCredentialActivity) { - mPromptInfo.setComponentNameForConfirmDeviceCredentialActivity( - componentNameForConfirmDeviceCredentialActivity); + public Builder setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) { + mPromptInfo.setRealCallerForConfirmDeviceCredentialActivity(realCaller); + mPromptInfo.setClassNameIfItIsConfirmDeviceCredentialActivity( + mContext.getClass().getName()); return this; } diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index 901f6b7ba5c1..df5d864196b4 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -57,7 +57,8 @@ public class PromptInfo implements Parcelable { private boolean mIsForLegacyFingerprintManager = false; private boolean mShowEmergencyCallButton = false; private boolean mUseParentProfileForDeviceCredential = false; - private ComponentName mComponentNameForConfirmDeviceCredentialActivity = null; + private ComponentName mRealCallerForConfirmDeviceCredentialActivity = null; + private String mClassNameIfItIsConfirmDeviceCredentialActivity = null; public PromptInfo() { @@ -89,8 +90,9 @@ public class PromptInfo implements Parcelable { mIsForLegacyFingerprintManager = in.readBoolean(); mShowEmergencyCallButton = in.readBoolean(); mUseParentProfileForDeviceCredential = in.readBoolean(); - mComponentNameForConfirmDeviceCredentialActivity = in.readParcelable( + mRealCallerForConfirmDeviceCredentialActivity = in.readParcelable( ComponentName.class.getClassLoader(), ComponentName.class); + mClassNameIfItIsConfirmDeviceCredentialActivity = in.readString(); } public static final Creator<PromptInfo> CREATOR = new Creator<PromptInfo>() { @@ -136,7 +138,8 @@ public class PromptInfo implements Parcelable { dest.writeBoolean(mIsForLegacyFingerprintManager); dest.writeBoolean(mShowEmergencyCallButton); dest.writeBoolean(mUseParentProfileForDeviceCredential); - dest.writeParcelable(mComponentNameForConfirmDeviceCredentialActivity, 0); + dest.writeParcelable(mRealCallerForConfirmDeviceCredentialActivity, 0); + dest.writeString(mClassNameIfItIsConfirmDeviceCredentialActivity); } // LINT.IfChange @@ -155,7 +158,7 @@ public class PromptInfo implements Parcelable { return true; } else if (mShowEmergencyCallButton) { return true; - } else if (mComponentNameForConfirmDeviceCredentialActivity != null) { + } else if (mRealCallerForConfirmDeviceCredentialActivity != null) { return true; } return false; @@ -321,10 +324,8 @@ public class PromptInfo implements Parcelable { mShowEmergencyCallButton = showEmergencyCallButton; } - public void setComponentNameForConfirmDeviceCredentialActivity( - ComponentName componentNameForConfirmDeviceCredentialActivity) { - mComponentNameForConfirmDeviceCredentialActivity = - componentNameForConfirmDeviceCredentialActivity; + public void setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) { + mRealCallerForConfirmDeviceCredentialActivity = realCaller; } public void setUseParentProfileForDeviceCredential( @@ -332,6 +333,14 @@ public class PromptInfo implements Parcelable { mUseParentProfileForDeviceCredential = useParentProfileForDeviceCredential; } + /** + * Set the class name of ConfirmDeviceCredentialActivity. + */ + void setClassNameIfItIsConfirmDeviceCredentialActivity(String className) { + mClassNameIfItIsConfirmDeviceCredentialActivity = className; + } + + // Getters /** @@ -455,8 +464,15 @@ public class PromptInfo implements Parcelable { return mShowEmergencyCallButton; } - public ComponentName getComponentNameForConfirmDeviceCredentialActivity() { - return mComponentNameForConfirmDeviceCredentialActivity; + public ComponentName getRealCallerForConfirmDeviceCredentialActivity() { + return mRealCallerForConfirmDeviceCredentialActivity; } + /** + * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is + * not ConfirmDeviceCredentialActivity. + */ + public String getClassNameIfItIsConfirmDeviceCredentialActivity() { + return mClassNameIfItIsConfirmDeviceCredentialActivity; + } } diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java index 3a254c1d92fc..bdda42a389eb 100644 --- a/core/java/android/security/net/config/SystemCertificateSource.java +++ b/core/java/android/security/net/config/SystemCertificateSource.java @@ -19,6 +19,8 @@ package android.security.net.config; import android.os.Environment; import android.os.UserHandle; +import com.android.internal.util.ArrayUtils; + import java.io.File; /** @@ -45,7 +47,7 @@ public final class SystemCertificateSource extends DirectoryCertificateSource { } File updatable_dir = new File("/apex/com.android.conscrypt/cacerts"); if (updatable_dir.exists() - && !(updatable_dir.list().length == 0)) { + && !(ArrayUtils.isEmpty(updatable_dir.list()))) { return updatable_dir; } return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"); diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 224379b4fd98..fc6c2e88779f 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -25,7 +25,6 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; -import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.SystemZenRules.PACKAGE_ANDROID; import static android.service.notification.ZenAdapters.peopleTypeToPrioritySenders; import static android.service.notification.ZenAdapters.prioritySendersToPeopleType; @@ -76,8 +75,9 @@ import android.util.PluralsMessageFormatter; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import androidx.annotation.VisibleForTesting; + import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -1074,7 +1074,7 @@ public class ZenModeConfig implements Parcelable { rt.manualRule.type = AutomaticZenRule.TYPE_OTHER; rt.manualRule.condition = new Condition( rt.manualRule.conditionId != null ? rt.manualRule.conditionId - : Uri.EMPTY, "", STATE_TRUE); + : Uri.EMPTY, "", Condition.STATE_TRUE); } } return rt; @@ -2540,10 +2540,34 @@ public class ZenModeConfig implements Parcelable { } public static class ZenRule implements Parcelable { + + /** No manual override. Rule owner can decide its state. */ + public static final int OVERRIDE_NONE = 0; + /** + * User has manually activated a mode. This will temporarily overrule the rule owner's + * decision to deactivate it (see {@link #reconsiderConditionOverride}). + */ + public static final int OVERRIDE_ACTIVATE = 1; + /** + * User has manually deactivated an active mode, or setting ZEN_MODE_OFF (for the few apps + * still allowed to do that) snoozed the mode. This will temporarily overrule the rule + * owner's decision to activate it (see {@link #reconsiderConditionOverride}). + */ + public static final int OVERRIDE_DEACTIVATE = 2; + + @IntDef(prefix = { "OVERRIDE" }, value = { + OVERRIDE_NONE, + OVERRIDE_ACTIVATE, + OVERRIDE_DEACTIVATE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ConditionOverride {} + @UnsupportedAppUsage public boolean enabled; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean snoozing; // user manually disabled this instance + @Deprecated + public boolean snoozing; // user manually disabled this instance. Obsolete with MODES_UI @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public String name; // required for automatic @UnsupportedAppUsage @@ -2579,6 +2603,15 @@ public class ZenModeConfig implements Parcelable { // ZenPolicy, so we store them here, only for the manual rule. @FlaggedApi(Flags.FLAG_MODES_UI) int legacySuppressedEffects; + /** + * Signals a user's action to (temporarily or permanently) activate or deactivate this + * rule, overruling the condition set by the owner. This value is not stored to disk, as + * it shouldn't survive reboots or be involved in B&R. It might be reset by certain + * owner-provided state transitions as well. + */ + @FlaggedApi(Flags.FLAG_MODES_UI) + @ConditionOverride + int conditionOverride = OVERRIDE_NONE; public ZenRule() { } @@ -2620,6 +2653,7 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { disabledOrigin = source.readInt(); legacySuppressedEffects = source.readInt(); + conditionOverride = source.readInt(); } } } @@ -2698,6 +2732,7 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { dest.writeInt(disabledOrigin); dest.writeInt(legacySuppressedEffects); + dest.writeInt(conditionOverride); } } } @@ -2708,9 +2743,16 @@ public class ZenModeConfig implements Parcelable { .append("id=").append(id) .append(",state=").append(condition == null ? "STATE_FALSE" : Condition.stateToString(condition.state)) - .append(",enabled=").append(String.valueOf(enabled).toUpperCase()) - .append(",snoozing=").append(snoozing) - .append(",name=").append(name) + .append(",enabled=").append(String.valueOf(enabled).toUpperCase()); + + if (Flags.modesUi()) { + sb.append(",conditionOverride=") + .append(conditionOverrideToString(conditionOverride)); + } else { + sb.append(",snoozing=").append(snoozing); + } + + sb.append(",name=").append(name) .append(",zenMode=").append(Global.zenModeToString(zenMode)) .append(",conditionId=").append(conditionId) .append(",pkg=").append(pkg) @@ -2753,6 +2795,15 @@ public class ZenModeConfig implements Parcelable { return sb.append(']').toString(); } + private static String conditionOverrideToString(@ConditionOverride int value) { + return switch(value) { + case OVERRIDE_ACTIVATE -> "OVERRIDE_ACTIVATE"; + case OVERRIDE_DEACTIVATE -> "OVERRIDE_DEACTIVATE"; + case OVERRIDE_NONE -> "OVERRIDE_NONE"; + default -> "UNKNOWN"; + }; + } + /** @hide */ // TODO: add configuration activity public void dumpDebug(ProtoOutputStream proto, long fieldId) { @@ -2763,7 +2814,11 @@ public class ZenModeConfig implements Parcelable { proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime); proto.write(ZenRuleProto.ENABLED, enabled); proto.write(ZenRuleProto.ENABLER, enabler); - proto.write(ZenRuleProto.IS_SNOOZING, snoozing); + if (Flags.modesApi() && Flags.modesUi()) { + proto.write(ZenRuleProto.IS_SNOOZING, conditionOverride == OVERRIDE_DEACTIVATE); + } else { + proto.write(ZenRuleProto.IS_SNOOZING, snoozing); + } proto.write(ZenRuleProto.ZEN_MODE, zenMode); if (conditionId != null) { proto.write(ZenRuleProto.CONDITION_ID, conditionId.toString()); @@ -2816,7 +2871,8 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { finalEquals = finalEquals && other.disabledOrigin == disabledOrigin - && other.legacySuppressedEffects == legacySuppressedEffects; + && other.legacySuppressedEffects == legacySuppressedEffects + && other.conditionOverride == conditionOverride; } } @@ -2832,7 +2888,8 @@ public class ZenModeConfig implements Parcelable { zenDeviceEffects, modified, allowManualInvocation, iconResName, triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, - deletionInstant, disabledOrigin, legacySuppressedEffects); + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride); } else { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, @@ -2858,8 +2915,74 @@ public class ZenModeConfig implements Parcelable { } } + // TODO: b/333527800 - Rename to isActive() public boolean isAutomaticActive() { - return enabled && !snoozing && getPkg() != null && isTrueOrUnknown(); + if (Flags.modesApi() && Flags.modesUi()) { + if (!enabled || getPkg() == null) { + return false; + } else if (conditionOverride == OVERRIDE_ACTIVATE) { + return true; + } else if (conditionOverride == OVERRIDE_DEACTIVATE) { + return false; + } else { + return isTrueOrUnknown(); + } + } else { + return enabled && !snoozing && getPkg() != null && isTrueOrUnknown(); + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @ConditionOverride + public int getConditionOverride() { + if (Flags.modesApi() && Flags.modesUi()) { + return conditionOverride; + } else { + return snoozing ? OVERRIDE_DEACTIVATE : OVERRIDE_NONE; + } + } + + public void setConditionOverride(@ConditionOverride int value) { + if (Flags.modesApi() && Flags.modesUi()) { + conditionOverride = value; + } else { + if (value == OVERRIDE_ACTIVATE) { + Slog.wtf(TAG, "Shouldn't set OVERRIDE_ACTIVATE if MODES_UI is off"); + } else if (value == OVERRIDE_DEACTIVATE) { + snoozing = true; + } else if (value == OVERRIDE_NONE) { + snoozing = false; + } + } + } + + public void resetConditionOverride() { + setConditionOverride(OVERRIDE_NONE); + } + + /** + * Possibly remove the override, depending on the rule owner's intended state. + * + * <p>This allows rule owners to "take over" manually-provided state with their smartness, + * but only once both agree. + * + * <p>For example, a manually activated rule wins over rule owner's opinion that it should + * be off, until the owner says it should be on, at which point it will turn off (without + * manual intervention) when the rule owner says it should be off. And symmetrically for + * manual deactivation (which used to be called "snoozing"). + */ + public void reconsiderConditionOverride() { + if (Flags.modesApi() && Flags.modesUi()) { + if (conditionOverride == OVERRIDE_ACTIVATE && isTrueOrUnknown()) { + resetConditionOverride(); + } else if (conditionOverride == OVERRIDE_DEACTIVATE && !isTrueOrUnknown()) { + resetConditionOverride(); + } + } else { + if (snoozing && !isTrueOrUnknown()) { + snoozing = false; + } + } } public String getPkg() { diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index a37e2277f0d1..05c2a9c26709 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -454,6 +454,8 @@ public class ZenModeDiff { */ public static class RuleDiff extends BaseDiff { public static final String FIELD_ENABLED = "enabled"; + public static final String FIELD_CONDITION_OVERRIDE = "conditionOverride"; + @Deprecated public static final String FIELD_SNOOZING = "snoozing"; public static final String FIELD_NAME = "name"; public static final String FIELD_ZEN_MODE = "zenMode"; @@ -507,8 +509,15 @@ public class ZenModeDiff { if (from.enabled != to.enabled) { addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled)); } - if (from.snoozing != to.snoozing) { - addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing)); + if (Flags.modesApi() && Flags.modesUi()) { + if (from.conditionOverride != to.conditionOverride) { + addField(FIELD_CONDITION_OVERRIDE, + new FieldDiff<>(from.conditionOverride, to.conditionOverride)); + } + } else { + if (from.snoozing != to.snoozing) { + addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing)); + } } if (!Objects.equals(from.name, to.name)) { addField(FIELD_NAME, new FieldDiff<>(from.name, to.name)); diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 09306c791537..288be9c392e1 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.TypedValue; +import android.view.WindowInsets; import dalvik.system.CloseGuard; @@ -881,12 +882,13 @@ public abstract class Animation implements Cloneable { } /** - * @return if a window animation has outsets applied to it. + * @return the edges to which outsets should be applied if run as a windoow animation. * * @hide */ - public boolean hasExtension() { - return false; + @WindowInsets.Side.InsetsSide + public int getExtensionEdges() { + return 0x0; } /** diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 5aaa994f3f8f..bbdc9d0392ba 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.RectF; import android.os.Build; import android.util.AttributeSet; +import android.view.WindowInsets; import java.util.ArrayList; import java.util.List; @@ -540,12 +541,12 @@ public class AnimationSet extends Animation { /** @hide */ @Override - public boolean hasExtension() { + @WindowInsets.Side.InsetsSide + public int getExtensionEdges() { + int edge = 0x0; for (Animation animation : mAnimations) { - if (animation.hasExtension()) { - return true; - } + edge |= animation.getExtensionEdges(); } - return false; + return edge; } } diff --git a/core/java/android/view/animation/ExtendAnimation.java b/core/java/android/view/animation/ExtendAnimation.java index 210eb8a1ca9d..1aeee07538f8 100644 --- a/core/java/android/view/animation/ExtendAnimation.java +++ b/core/java/android/view/animation/ExtendAnimation.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Insets; import android.util.AttributeSet; +import android.view.WindowInsets; /** * An animation that controls the outset of an object. @@ -151,9 +152,12 @@ public class ExtendAnimation extends Animation { /** @hide */ @Override - public boolean hasExtension() { - return mFromInsets.left < 0 || mFromInsets.top < 0 || mFromInsets.right < 0 - || mFromInsets.bottom < 0; + @WindowInsets.Side.InsetsSide + public int getExtensionEdges() { + return (mFromInsets.left < 0 || mToInsets.left < 0 ? WindowInsets.Side.LEFT : 0) + | (mFromInsets.right < 0 || mToInsets.right < 0 ? WindowInsets.Side.RIGHT : 0) + | (mFromInsets.top < 0 || mToInsets.top < 0 ? WindowInsets.Side.TOP : 0) + | (mFromInsets.bottom < 0 || mToInsets.bottom < 0 ? WindowInsets.Side.BOTTOM : 0); } @Override diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index a6ae948604a5..adbc59867c0c 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -61,16 +61,6 @@ flag { flag { namespace: "windowing_sdk" - name: "fix_pip_restore_to_overlay" - description: "Restore exit-pip activity back to ActivityEmbedding overlay" - bug: "297887697" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - namespace: "windowing_sdk" name: "activity_embedding_animation_customization_flag" description: "Whether the animation customization feature for AE is enabled" bug: "293658614" @@ -128,3 +118,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "ae_back_stack_restore" + description: "Allow the ActivityEmbedding back stack to be restored after process restarted" + bug: "289875940" + is_fixed_read_only: true +} diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 69d1cb34005d..7bfb8003fd18 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -210,8 +210,16 @@ public class Cuj { */ public static final int CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE = 116; + /** + * Track interaction of exiting desktop mode on closing the last window. + * + * <p>Tracking starts when the last window is closed and finishes when the animation to exit + * desktop mode ends. + */ + public static final int CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE = 117; + // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. - @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE; + @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; /** @hide */ @IntDef({ @@ -319,7 +327,8 @@ public class Cuj { CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN, CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE, CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH, - CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, + CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE }) @Retention(RetentionPolicy.SOURCE) public @interface CujType {} @@ -438,6 +447,7 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; } private Cuj() { @@ -666,6 +676,8 @@ public class Cuj { return "LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH"; case CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE: return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE"; + case CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE: + return "DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE"; } return "UNKNOWN"; } diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index 12804d43b04e..e7f0560612cc 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -163,7 +163,7 @@ message VibratorManagerServiceDumpProto { optional bool vibrator_under_external_control = 5; optional bool low_power_mode = 6; optional bool vibrate_on = 24; - optional bool keyboard_vibration_on = 25; + reserved 25; // prev keyboard_vibration_on optional int32 default_vibration_amplitude = 26; optional int32 alarm_intensity = 18; optional int32 alarm_default_intensity = 19; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 25e710784a8f..26d180cdcb1a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -695,12 +695,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen break; case TYPE_ACTIVITY_REPARENTED_TO_TASK: final IBinder candidateAssociatedActToken, lastOverlayToken; - if (Flags.fixPipRestoreToOverlay()) { - candidateAssociatedActToken = change.getOtherActivityToken(); - lastOverlayToken = change.getTaskFragmentToken(); - } else { - candidateAssociatedActToken = lastOverlayToken = null; - } + candidateAssociatedActToken = change.getOtherActivityToken(); + lastOverlayToken = change.getTaskFragmentToken(); onActivityReparentedToTask( wct, taskId, @@ -1023,10 +1019,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable OverlayContainerRestoreParams getOverlayContainerRestoreParams( @Nullable IBinder associatedActivityToken, @Nullable IBinder overlayToken) { - if (!Flags.fixPipRestoreToOverlay()) { - return null; - } - if (associatedActivityToken == null || overlayToken == null) { return null; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index d0e2c998e961..ee3e6f368505 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -36,7 +36,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; @@ -257,7 +256,7 @@ class TaskFragmentContainer { mPendingAppearedIntent = pendingAppearedIntent; // Save the information necessary for restoring the overlay when needed. - if (Flags.fixPipRestoreToOverlay() && overlayTag != null && pendingAppearedIntent != null + if (overlayTag != null && pendingAppearedIntent != null && associatedActivity != null && !associatedActivity.isFinishing()) { final IBinder associatedActivityToken = associatedActivity.getActivityToken(); final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken, diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 475475b05272..90eeb583d070 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -874,8 +874,6 @@ public class OverlayPresentationTest { @Test public void testOnActivityReparentedToTask_overlayRestoration() { - mSetFlagRule.enableFlags(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY); - // Prepares and mock the data necessary for the test. final IBinder activityToken = mActivity.getActivityToken(); final Intent intent = new Intent(); diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 5e673338bad3..84f7bb27ca82 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -53,6 +53,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import com.android.wm.shell.common.bubbles.BubbleBarLocation import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -458,5 +459,7 @@ class BubbleStackViewTest { override fun isShowingAsBubbleBar(): Boolean = false override fun hideCurrentInputMethod() {} + + override fun updateBubbleBarLocation(location: BubbleBarLocation) {} } } diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index bc59a235517d..debcba071d9c 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -42,6 +42,8 @@ <item type="id" name="action_move_top_right"/> <item type="id" name="action_move_bottom_left"/> <item type="id" name="action_move_bottom_right"/> + <item type="id" name="action_move_bubble_bar_left"/> + <item type="id" name="action_move_bubble_bar_right"/> <item type="id" name="dismiss_view"/> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index df5f1e46a03c..6a62d7a373c8 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -143,6 +143,10 @@ <string name="bubble_accessibility_action_expand_menu">expand menu</string> <!-- Click action label for bubbles to collapse menu. [CHAR LIMIT=30]--> <string name="bubble_accessibility_action_collapse_menu">collapse menu</string> + <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] --> + <string name="bubble_accessibility_action_move_bar_left">Move left</string> + <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] --> + <string name="bubble_accessibility_action_move_bar_right">Move right</string> <!-- Accessibility announcement when the stack of bubbles expands. [CHAR LIMIT=NONE]--> <string name="bubble_accessibility_announce_expand">expand <xliff:g id="bubble_title" example="Messages">%1$s</xliff:g></string> <!-- Accessibility announcement when the stack of bubbles collapses. [CHAR LIMIT=NONE]--> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 8d30db64a3e5..53551387230c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -18,6 +18,7 @@ package com.android.wm.shell.activityembedding; import static android.graphics.Matrix.MTRANS_X; import static android.graphics.Matrix.MTRANS_Y; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import android.annotation.CallSuper; import android.graphics.Point; @@ -146,6 +147,14 @@ class ActivityEmbeddingAnimationAdapter { /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { // Update the surface position and alpha. + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mAnimation.getExtensionEdges() != 0x0 + && !(mChange.hasFlags(FLAG_TRANSLUCENT) + && mChange.getActivityComponent() != null)) { + // Extend non-translucent activities + t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges()); + } + mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); @@ -165,7 +174,7 @@ class ActivityEmbeddingAnimationAdapter { if (!cropRect.intersect(mWholeAnimationBounds)) { // Hide the surface when it is outside of the animation area. t.setAlpha(mLeash, 0); - } else if (mAnimation.hasExtension()) { + } else if (mAnimation.getExtensionEdges() != 0) { // Allow the surface to be shown in its original bounds in case we want to use edge // extensions. cropRect.union(mContentBounds); @@ -180,6 +189,10 @@ class ActivityEmbeddingAnimationAdapter { @CallSuper void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { onAnimationUpdate(t, mAnimation.getDuration()); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mAnimation.getExtensionEdges() != 0x0) { + t.setEdgeExtensionEffect(mLeash, /* edge */ 0); + } } final long getDurationHint() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 5696a544152c..d2cef4baf798 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -144,8 +144,10 @@ class ActivityEmbeddingAnimationRunner { // ending states. prepareForJumpCut(info, startTransaction); } else { - addEdgeExtensionIfNeeded(startTransaction, finishTransaction, - postStartTransactionCallbacks, adapters); + if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { + addEdgeExtensionIfNeeded(startTransaction, finishTransaction, + postStartTransactionCallbacks, adapters); + } addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters); for (ActivityEmbeddingAnimationAdapter adapter : adapters) { duration = Math.max(duration, adapter.getDurationHint()); @@ -341,7 +343,7 @@ class ActivityEmbeddingAnimationRunner { @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { for (ActivityEmbeddingAnimationAdapter adapter : adapters) { final Animation animation = adapter.mAnimation; - if (!animation.hasExtension()) { + if (animation.getExtensionEdges() == 0) { continue; } if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 7275c6494140..12422874ca5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -95,6 +95,7 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; /** * Controls the window animation run when a user initiates a back gesture. @@ -1209,7 +1210,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION - && !isGestureBackTransition(info)) { + && isNotGestureBackTransition(info)) { return false; } @@ -1364,7 +1365,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // try to handle unexpected transition mergePendingTransitions(info); - if (!isGestureBackTransition(info) || shouldCancelAnimation(info) + if (isNotGestureBackTransition(info) || shouldCancelAnimation(info) || !mCloseTransitionRequested) { if (mPrepareOpenTransition != null) { applyFinishOpenTransition(); @@ -1395,8 +1396,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Cancel close animation if something happen unexpected, let another handler to handle private boolean shouldCancelAnimation(@NonNull TransitionInfo info) { - final boolean noCloseAllowed = - info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; + final boolean noCloseAllowed = !mCloseTransitionRequested + && info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; boolean unableToHandle = false; boolean filterTargets = false; for (int i = info.getChanges().size() - 1; i >= 0; --i) { @@ -1455,7 +1456,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { return false; } - + // Must have open target, must not have close target. + if (hasAnimationInMode(info, TransitionUtil::isClosingMode) + || !hasAnimationInMode(info, TransitionUtil::isOpeningMode)) { + return false; + } SurfaceControl openingLeash = null; if (mApps != null) { for (int i = mApps.length - 1; i >= 0; --i) { @@ -1482,17 +1487,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return true; } - private boolean isGestureBackTransition(@NonNull TransitionInfo info) { - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change c = info.getChanges().get(i); - if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) - && (TransitionUtil.isOpeningMode(c.getMode()) - || TransitionUtil.isClosingMode(c.getMode()))) { - return true; - } - } - return false; - } /** * Check whether this transition is triggered from back gesture commitment. * Reparent the transition targets to animation leashes, so the animation won't be broken. @@ -1501,10 +1495,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION - || !mCloseTransitionRequested) { + if (!mCloseTransitionRequested) { return false; } + // must have close target + if (!hasAnimationInMode(info, TransitionUtil::isClosingMode)) { + return false; + } + if (mApps == null) { + // animation is done + applyAndFinish(st, ft, finishCallback); + return true; + } SurfaceControl openingLeash = null; SurfaceControl closingLeash = null; for (int i = mApps.length - 1; i >= 0; --i) { @@ -1522,6 +1524,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final Point offset = c.getEndRelOffset(); st.setPosition(c.getLeash(), offset.x, offset.y); st.reparent(c.getLeash(), openingLeash); + st.setAlpha(c.getLeash(), 1.0f); } else if (TransitionUtil.isClosingMode(c.getMode())) { st.reparent(c.getLeash(), closingLeash); } @@ -1592,6 +1595,21 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } + private static boolean isNotGestureBackTransition(@NonNull TransitionInfo info) { + return !hasAnimationInMode(info, TransitionUtil::isOpenOrCloseMode); + } + + private static boolean hasAnimationInMode(@NonNull TransitionInfo info, + Predicate<Integer> mode) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && mode.test(c.getMode())) { + return true; + } + } + return false; + } + private static ComponentName findComponentName(TransitionInfo.Change change) { final ComponentName componentName = change.getActivityComponent(); if (componentName != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index f9a1d940c734..dc511be59764 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -357,7 +357,9 @@ public class BadgedImageView extends ConstraintLayout { void showBadge() { Bitmap appBadgeBitmap = mBubble.getAppBadge(); - if (appBadgeBitmap == null) { + final boolean isAppLaunchIntent = (mBubble instanceof Bubble) + && ((Bubble) mBubble).isAppLaunchIntent(); + if (appBadgeBitmap == null || isAppLaunchIntent) { mAppIcon.setVisibility(GONE); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 7dbbb04e4406..5cd2cb7d51d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -50,6 +50,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.common.bubbles.BubbleInfo; @@ -246,7 +247,23 @@ public class Bubble implements BubbleViewProvider { mAppIntent = intent; mDesiredHeight = Integer.MAX_VALUE; mPackageName = intent.getPackage(); + } + private Bubble(ShortcutInfo info, Executor mainExecutor) { + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = info.getUserHandle(); + mIcon = info.getIcon(); + mIsAppBubble = false; + mKey = getBubbleKeyForShortcut(info); + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mTaskId = INVALID_TASK_ID; + mAppIntent = null; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = info.getPackage(); + mShortcutInfo = info; } /** Creates an app bubble. */ @@ -263,6 +280,13 @@ public class Bubble implements BubbleViewProvider { mainExecutor); } + /** Creates a shortcut bubble. */ + public static Bubble createShortcutBubble( + ShortcutInfo info, + Executor mainExecutor) { + return new Bubble(info, mainExecutor); + } + /** * Returns the key for an app bubble from an app with package name, {@code packageName} on an * Android user, {@code user}. @@ -273,6 +297,14 @@ public class Bubble implements BubbleViewProvider { return KEY_APP_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; } + /** + * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the + * {@code shortcutInfo} id. + */ + public static String getBubbleKeyForShortcut(ShortcutInfo info) { + return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); + } + @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, @@ -888,6 +920,17 @@ public class Bubble implements BubbleViewProvider { return mIntent; } + /** + * Whether this bubble represents the full app, i.e. the intent used is the launch + * intent for an app. In this case we don't show a badge on the icon. + */ + public boolean isAppLaunchIntent() { + if (Flags.enableBubbleAnything() && mAppIntent != null) { + return mAppIntent.hasCategory("android.intent.category.LAUNCHER"); + } + return false; + } + @Nullable PendingIntent getDeleteIntent() { return mDeleteIntent; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 949a7236434a..29520efd70b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1335,6 +1335,40 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands and selects a bubble created or found via the provided shortcut info. + * + * @param info the shortcut info for the bubble. + */ + public void expandStackAndSelectBubble(ShortcutInfo info) { + if (!Flags.enableBubbleAnything()) return; + Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** + * Expands and selects a bubble created or found for this app. + * + * @param intent the intent for the bubble. + */ + public void expandStackAndSelectBubble(Intent intent) { + if (!Flags.enableBubbleAnything()) return; + Bubble b = mBubbleData.getOrCreateBubble(intent); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble * exists for this entry, and it is able to bubble, a new bubble will be created. * @@ -2323,6 +2357,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param entry the entry to bubble. */ static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { + if (Flags.enableBubbleAnything()) return true; PendingIntent intent = entry.getBubbleMetadata() != null ? entry.getBubbleMetadata().getIntent() : null; @@ -2439,6 +2474,16 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void showShortcutBubble(ShortcutInfo info) { + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info)); + } + + @Override + public void showAppBubble(Intent intent) { + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent)); + } + + @Override public void showBubble(String key, int topOnScreen) { mMainExecutor.execute( () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen)); @@ -2634,6 +2679,13 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void expandStackAndSelectBubble(ShortcutInfo info) { + mMainExecutor.execute(() -> { + BubbleController.this.expandStackAndSelectBubble(info); + }); + } + + @Override public void expandStackAndSelectBubble(Bubble bubble) { mMainExecutor.execute(() -> { BubbleController.this.expandStackAndSelectBubble(bubble); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index b6da761b0f9c..3c6c6fa0d8d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -23,8 +23,10 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.LocusId; import android.content.pm.ShortcutInfo; +import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -421,23 +423,19 @@ public class BubbleData { Bubble bubbleToReturn = getBubbleInStackWithKey(key); if (bubbleToReturn == null) { - bubbleToReturn = getOverflowBubbleWithKey(key); - if (bubbleToReturn != null) { - // Promoting from overflow - mOverflowBubbles.remove(bubbleToReturn); - if (mOverflowBubbles.isEmpty()) { - mStateChange.showOverflowChanged = true; + // Check if it's in the overflow + bubbleToReturn = findAndRemoveBubbleFromOverflow(key); + if (bubbleToReturn == null) { + if (entry != null) { + // Not in the overflow, have an entry, so it's a new bubble + bubbleToReturn = new Bubble(entry, + mBubbleMetadataFlagListener, + mCancelledListener, + mMainExecutor); + } else { + // If there's no entry it must be a persisted bubble + bubbleToReturn = persistedBubble; } - } else if (mPendingBubbles.containsKey(key)) { - // Update while it was pending - bubbleToReturn = mPendingBubbles.get(key); - } else if (entry != null) { - // New bubble - bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, - mMainExecutor); - } else { - // Persisted bubble being promoted - bubbleToReturn = persistedBubble; } } @@ -448,6 +446,46 @@ public class BubbleData { return bubbleToReturn; } + Bubble getOrCreateBubble(ShortcutInfo info) { + String bubbleKey = Bubble.getBubbleKeyForShortcut(info); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createShortcutBubble(info, mMainExecutor); + } + return bubbleToReturn; + } + + Bubble getOrCreateBubble(Intent intent) { + UserHandle user = UserHandle.of(mCurrentUserId); + String bubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), + user); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createAppBubble(intent, user, null, mMainExecutor); + } + return bubbleToReturn; + } + + @Nullable + private Bubble findAndRemoveBubbleFromOverflow(String key) { + Bubble bubbleToReturn = getBubbleInStackWithKey(key); + if (bubbleToReturn != null) { + return bubbleToReturn; + } + bubbleToReturn = getOverflowBubbleWithKey(key); + if (bubbleToReturn != null) { + mOverflowBubbles.remove(bubbleToReturn); + // Promoting from overflow + mOverflowBubbles.remove(bubbleToReturn); + if (mOverflowBubbles.isEmpty()) { + mStateChange.showOverflowChanged = true; + } + } else if (mPendingBubbles.containsKey(key)) { + bubbleToReturn = mPendingBubbles.get(key); + } + return bubbleToReturn; + } + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index fdb45239fa63..a0c0a25d97a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -232,6 +232,9 @@ public class BubbleExpandedView extends LinearLayout { fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); + if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( @@ -246,7 +249,8 @@ public class BubbleExpandedView extends LinearLayout { /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); - } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + } else if (!mIsOverflow && isShortcutBubble) { + ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey()); options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index 3d9bf032c1b0..4e80e903b522 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles +import com.android.wm.shell.common.bubbles.BubbleBarLocation + /** Manager interface for bubble expanded views. */ interface BubbleExpandedViewManager { @@ -30,6 +32,7 @@ interface BubbleExpandedViewManager { fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() + fun updateBubbleBarLocation(location: BubbleBarLocation) companion object { /** @@ -78,6 +81,10 @@ interface BubbleExpandedViewManager { override fun hideCurrentInputMethod() { controller.hideCurrentInputMethod() } + + override fun updateBubbleBarLocation(location: BubbleBarLocation) { + controller.bubbleBarLocation = location + } } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index efa1031bf814..f002d8904626 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1601,6 +1601,11 @@ public class BubbleStackView extends FrameLayout getResources().getColor(android.R.color.system_neutral1_1000))); mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( getResources().getColor(android.R.color.system_neutral1_1000))); + if (mShowingManage) { + // the manage menu location depends on the manage button location which may need a + // layout pass, so post this to the looper + post(() -> showManageMenu(true)); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 5e2141aa639e..5f8f0fd0c54c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -36,6 +36,7 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.taskview.TaskView; /** @@ -110,6 +111,8 @@ public class BubbleTaskViewHelper { fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( @@ -124,7 +127,7 @@ public class BubbleTaskViewHelper { /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); - } else if (mBubble.hasMetadataShortcutId()) { + } else if (isShortcutBubble) { options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 589dfd24624e..9a27fb65ac2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -23,6 +23,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.drawable.Icon; import android.hardware.HardwareBuffer; @@ -118,6 +119,14 @@ public interface Bubbles { /** * Request the stack expand if needed, then select the specified Bubble as current. + * If no bubble exists for this entry, one is created. + * + * @param info the shortcut info to use to create the bubble. + */ + void expandStackAndSelectBubble(ShortcutInfo info); + + /** + * Request the stack expand if needed, then select the specified Bubble as current. * * @param bubble the bubble to be selected */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 0907ddd1de83..5c789749412c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles; import android.content.Intent; import android.graphics.Rect; +import android.content.pm.ShortcutInfo; import com.android.wm.shell.bubbles.IBubblesListener; import com.android.wm.shell.common.bubbles.BubbleBarLocation; @@ -48,4 +49,8 @@ interface IBubbles { oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10; oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 11; + + oneway void showShortcutBubble(in ShortcutInfo info) = 12; + + oneway void showAppBubble(in Intent intent) = 13; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index b7834dbcf07f..6d868d215482 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -26,6 +26,7 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Outline; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.FloatProperty; import android.view.LayoutInflater; @@ -35,6 +36,8 @@ import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleExpandedViewManager; @@ -43,6 +46,7 @@ import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.taskview.TaskView; import java.util.function.Supplier; @@ -82,6 +86,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); private static final int INVALID_TASK_ID = -1; + private Bubble mBubble; private BubbleExpandedViewManager mManager; private BubblePositioner mPositioner; private boolean mIsOverflow; @@ -190,16 +195,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); - mHandleView.setAccessibilityDelegate(new AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, - AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - info.addAction(new AccessibilityNodeInfo.AccessibilityAction( - AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( - R.string.bubble_accessibility_action_expand_menu))); - } - }); + mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate()); } mMenuViewController = new BubbleBarMenuViewController(mContext, this); mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { @@ -338,6 +334,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView /** Updates the bubble shown in the expanded view. */ public void update(Bubble bubble) { + mBubble = bubble; mBubbleTaskViewHelper.update(bubble); mMenuViewController.updateMenu(bubble); } @@ -476,4 +473,51 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView invalidateOutline(); } } + + private class HandleViewAccessibilityDelegate extends AccessibilityDelegate { + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull View host, + @NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( + R.string.bubble_accessibility_action_expand_menu))); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); + if (mPositioner.isBubbleBarOnLeft()) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + R.id.action_move_bubble_bar_right, getResources().getString( + R.string.bubble_accessibility_action_move_bar_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + R.id.action_move_bubble_bar_left, getResources().getString( + R.string.bubble_accessibility_action_move_bar_left))); + } + } + + @Override + public boolean performAccessibilityAction(@NonNull View host, int action, + @Nullable Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { + mManager.collapseStack(); + return true; + } + if (action == AccessibilityNodeInfo.ACTION_DISMISS) { + mManager.dismissBubble(mBubble, Bubbles.DISMISS_USER_GESTURE); + return true; + } + if (action == R.id.action_move_bubble_bar_left) { + mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT); + return true; + } + if (action == R.id.action_move_bubble_bar_right) { + mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + return true; + } + return false; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 400882a8da9a..05c9d02a0de7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -73,25 +73,9 @@ class DesktopModeEventLogger { sessionId, taskUpdate.instanceId ) - FrameworkStatsLog.write( - DESKTOP_MODE_TASK_UPDATE_ATOM_ID, - /* task_event */ + logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, - /* instance_id */ - taskUpdate.instanceId, - /* uid */ - taskUpdate.uid, - /* task_height */ - taskUpdate.taskHeight, - /* task_width */ - taskUpdate.taskWidth, - /* task_x */ - taskUpdate.taskX, - /* task_y */ - taskUpdate.taskY, - /* session_id */ - sessionId - ) + sessionId, taskUpdate) } /** @@ -105,25 +89,9 @@ class DesktopModeEventLogger { sessionId, taskUpdate.instanceId ) - FrameworkStatsLog.write( - DESKTOP_MODE_TASK_UPDATE_ATOM_ID, - /* task_event */ + logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, - /* instance_id */ - taskUpdate.instanceId, - /* uid */ - taskUpdate.uid, - /* task_height */ - taskUpdate.taskHeight, - /* task_width */ - taskUpdate.taskWidth, - /* task_x */ - taskUpdate.taskX, - /* task_y */ - taskUpdate.taskY, - /* session_id */ - sessionId - ) + sessionId, taskUpdate) } /** @@ -137,10 +105,16 @@ class DesktopModeEventLogger { sessionId, taskUpdate.instanceId ) + logTaskUpdate( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + sessionId, taskUpdate) + } + + private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) { FrameworkStatsLog.write( DESKTOP_MODE_TASK_UPDATE_ATOM_ID, /* task_event */ - FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + taskEvent, /* instance_id */ taskUpdate.instanceId, /* uid */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index 73aa7ceea68d..a6ed3b8cb50c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -22,6 +22,7 @@ import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder +import android.os.Trace import android.util.SparseArray import android.view.SurfaceControl import android.view.WindowManager @@ -51,6 +52,8 @@ import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +const val VISIBLE_TASKS_COUNTER_NAME = "DESKTOP_MODE_VISIBLE_TASKS" + /** * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log * appropriate desktop mode session log events. This observes transitions related to desktop mode @@ -292,8 +295,14 @@ class DesktopModeLoggerTransitionObserver( val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId] when { // new tasks added - previousTaskInfo == null -> + previousTaskInfo == null -> { desktopModeEventLogger.logTaskAdded(sessionId, currentTaskUpdate) + Trace.setCounter( + Trace.TRACE_TAG_WINDOW_MANAGER, + VISIBLE_TASKS_COUNTER_NAME, + postTransitionVisibleFreeformTasks.size().toLong() + ) + } // old tasks that were resized or repositioned // TODO(b/347935387): Log changes only once they are stable. buildTaskUpdateForTask(previousTaskInfo) != currentTaskUpdate -> @@ -305,6 +314,11 @@ class DesktopModeLoggerTransitionObserver( preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo)) + Trace.setCounter( + Trace.TRACE_TAG_WINDOW_MANAGER, + VISIBLE_TASKS_COUNTER_NAME, + postTransitionVisibleFreeformTasks.size().toLong() + ) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index 38675129ce57..597637d3fbfc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -70,11 +70,11 @@ class DesktopTasksLimiter ( // TODO(b/333018485): replace this observer when implementing the minimize-animation private inner class MinimizeTransitionObserver : TransitionObserver { - private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() - private val mActiveTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + private val pendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + private val activeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) { - mPendingTransitionTokensAndTasks[transition] = taskDetails + pendingTransitionTokensAndTasks[transition] = taskDetails } override fun onTransitionReady( @@ -83,9 +83,7 @@ class DesktopTasksLimiter ( startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction ) { - val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return - taskToMinimize.transitionInfo = info - mActiveTransitionTokensAndTasks[transition] = taskToMinimize + val taskToMinimize = pendingTransitionTokensAndTasks.remove(transition) ?: return if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return @@ -97,6 +95,8 @@ class DesktopTasksLimiter ( return } + taskToMinimize.transitionInfo = info + activeTransitionTokensAndTasks[transition] = taskToMinimize this@DesktopTasksLimiter.markTaskMinimized( taskToMinimize.displayId, taskToMinimize.taskId) } @@ -121,7 +121,7 @@ class DesktopTasksLimiter ( } override fun onTransitionStarting(transition: IBinder) { - val mActiveTaskDetails = mActiveTransitionTokensAndTasks[transition] + val mActiveTaskDetails = activeTransitionTokensAndTasks[transition] if (mActiveTaskDetails != null && mActiveTaskDetails.transitionInfo != null) { // Begin minimize window CUJ instrumentation. interactionJankMonitor.begin( @@ -132,11 +132,11 @@ class DesktopTasksLimiter ( } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { - if (mActiveTransitionTokensAndTasks.remove(merged) != null) { + if (activeTransitionTokensAndTasks.remove(merged) != null) { interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) } - mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> - mPendingTransitionTokensAndTasks[playing] = taskToTransfer + pendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> + pendingTransitionTokensAndTasks[playing] = taskToTransfer } } @@ -144,14 +144,14 @@ class DesktopTasksLimiter ( ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: transition %s finished", transition) - if (mActiveTransitionTokensAndTasks.remove(transition) != null) { + if (activeTransitionTokensAndTasks.remove(transition) != null) { if (aborted) { interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) } else { interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) } } - mPendingTransitionTokensAndTasks.remove(transition) + pendingTransitionTokensAndTasks.remove(transition) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 778478405dda..de6887a2173b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -502,15 +502,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a, backgroundColorForTransition); - if (!isTask && a.hasExtension()) { - if (!TransitionUtil.isOpeningType(mode)) { - // Can screenshot now (before startTransaction is applied) - edgeExtendWindow(change, a, startTransaction, finishTransaction); + if (!isTask && a.getExtensionEdges() != 0x0) { + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { + finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0); } else { - // Need to screenshot after startTransaction is applied otherwise activity - // may not be visible or ready yet. - postStartTransactionCallbacks - .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + if (!TransitionUtil.isOpeningType(mode)) { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, a, startTransaction, finishTransaction); + } else { + // Need to screenshot after startTransaction is applied otherwise + // activity may not be visible or ready yet. + postStartTransactionCallbacks + .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + } } } @@ -558,7 +562,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, - clipRect); + clipRect, change.getActivityComponent() != null); final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { @@ -823,7 +827,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Animation anim, @NonNull SurfaceControl leash, @NonNull Runnable finishCallback, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius, - @Nullable Rect clipRect) { + @Nullable Rect clipRect, boolean isActivity) { final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); @@ -835,13 +839,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, - position, cornerRadius, clipRect); + position, cornerRadius, clipRect, isActivity); }; va.addUpdateListener(updateListener); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, - position, cornerRadius, clipRect); + position, cornerRadius, clipRect, isActivity); pool.release(transaction); mainExecutor.execute(() -> { @@ -931,7 +935,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds(), + change.getActivityComponent() != null); } private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, @@ -955,7 +960,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds(), + change.getActivityComponent() != null); } private static int getWallpaperTransitType(TransitionInfo info) { @@ -1005,9 +1011,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static void applyTransformation(long time, SurfaceControl.Transaction t, SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix, - Point position, float cornerRadius, @Nullable Rect immutableClipRect) { + Point position, float cornerRadius, @Nullable Rect immutableClipRect, + boolean isActivity) { tmpTransformation.clear(); anim.getTransformation(time, tmpTransformation); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && anim.getExtensionEdges() != 0x0 && isActivity) { + t.setEdgeExtensionEffect(leash, anim.getExtensionEdges()); + } if (position != null) { tmpTransformation.getMatrix().postTranslate(position.x, position.y); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index e196254628d0..195882553602 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -325,21 +325,21 @@ class ScreenRotationAnimation { @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void buildScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt index a5e0550d9c79..3ffc9d7b87f6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt @@ -21,6 +21,7 @@ import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.traces.component.ComponentNameMatcher +import com.android.wm.shell.Flags import com.android.wm.shell.flicker.pip.common.ClosePipTransition import org.junit.FixMethodOrder import org.junit.Test @@ -60,7 +61,7 @@ class ClosePipBySwipingDownTest(flicker: LegacyFlickerTest) : ClosePipTransition val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 val barComponent = - if (flicker.scenario.isTablet) { + if (flicker.scenario.isTablet || Flags.enableTaskbarOnPhones()) { ComponentNameMatcher.TASK_BAR } else { ComponentNameMatcher.NAV_BAR diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt index dad5db94d062..a9dba4a3178b 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt @@ -17,11 +17,13 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.Presubmit +import android.tools.NavBar import android.tools.Rotation +import android.tools.ScenarioBuilder import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest -import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.SERVICE_TRACE_CONFIG import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark @@ -35,7 +37,7 @@ import org.junit.runners.Parameterized /** * Test quick switch between two split pairs. * - * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen` + * To run this test: `atest WMShellFlickerTestsSplitScreenGroupOther:MultipleShowImeRequestsInSplitScreen` */ @RequiresDevice @RunWith(Parameterized::class) @@ -58,10 +60,22 @@ class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTe } companion object { + private fun createFlickerTest( + navBarMode: NavBar + ) = LegacyFlickerTest(ScenarioBuilder() + .withStartRotation(Rotation.ROTATION_0) + .withEndRotation(Rotation.ROTATION_0) + .withNavBarMode(navBarMode), resultReaderProvider = { scenario -> + android.tools.flicker.datastore.CachedResultReader( + scenario, SERVICE_TRACE_CONFIG + ) + }) + @Parameterized.Parameters(name = "{0}") @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0) + fun getParams() = listOf( + createFlickerTest(NavBar.MODE_GESTURAL), + createFlickerTest(NavBar.MODE_3BUTTON) ) } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt index 4465a16a8e0f..acaf021981ed 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt @@ -28,12 +28,16 @@ import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible +import com.android.wm.shell.Flags import org.junit.Assume import org.junit.Test interface ICommonAssertions { val flicker: LegacyFlickerTest + val usesTaskbar: Boolean + get() = flicker.scenario.isTablet || Flags.enableTaskbarOnPhones() + /** Checks that all parts of the screen are covered during the transition */ @Presubmit @Test fun entireScreenCovered() = flicker.entireScreenCovered() @@ -43,7 +47,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtStartAndEnd() } @@ -54,7 +58,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarLayerPositionAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtStartAndEnd() } @@ -66,7 +70,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarWindowIsAlwaysVisible() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarWindowIsAlwaysVisible() } @@ -76,7 +80,7 @@ interface ICommonAssertions { @Presubmit @Test fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarLayerIsVisibleAtStartAndEnd() } @@ -88,7 +92,7 @@ interface ICommonAssertions { @Presubmit @Test fun taskBarWindowIsAlwaysVisible() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarWindowIsAlwaysVisible() } diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp index bedfc49a1b0d..43b56c32fb79 100644 --- a/libs/androidfw/BigBuffer.cpp +++ b/libs/androidfw/BigBuffer.cpp @@ -17,8 +17,8 @@ #include <androidfw/BigBuffer.h> #include <algorithm> +#include <iterator> #include <memory> -#include <vector> #include "android-base/logging.h" @@ -78,10 +78,27 @@ void* BigBuffer::NextBlock(size_t* out_size) { std::string BigBuffer::to_string() const { std::string result; + result.reserve(size_); for (const Block& block : blocks_) { result.append(block.buffer.get(), block.buffer.get() + block.size); } return result; } +void BigBuffer::AppendBuffer(BigBuffer&& buffer) { + std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); + size_ += buffer.size_; + buffer.blocks_.clear(); + buffer.size_ = 0; +} + +void BigBuffer::BackUp(size_t count) { + Block& block = blocks_.back(); + block.size -= count; + size_ -= count; + // BigBuffer is supposed to always give zeroed memory, but backing up usually means + // something has been already written into the block. Erase it. + std::fill_n(block.buffer.get() + block.size, count, 0); +} + } // namespace android diff --git a/libs/androidfw/include/androidfw/BigBuffer.h b/libs/androidfw/include/androidfw/BigBuffer.h index b99a4edf9d88..c4cd7c576542 100644 --- a/libs/androidfw/include/androidfw/BigBuffer.h +++ b/libs/androidfw/include/androidfw/BigBuffer.h @@ -14,13 +14,12 @@ * limitations under the License. */ -#ifndef _ANDROID_BIG_BUFFER_H -#define _ANDROID_BIG_BUFFER_H +#pragma once -#include <cstring> #include <memory> #include <string> #include <type_traits> +#include <utility> #include <vector> #include "android-base/logging.h" @@ -150,24 +149,11 @@ inline size_t BigBuffer::block_size() const { template <typename T> inline T* BigBuffer::NextBlock(size_t count) { - static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type"); + static_assert(std::is_standard_layout_v<T>, "T must be standard_layout type"); CHECK(count != 0); return reinterpret_cast<T*>(NextBlockImpl(sizeof(T) * count)); } -inline void BigBuffer::BackUp(size_t count) { - Block& block = blocks_.back(); - block.size -= count; - size_ -= count; -} - -inline void BigBuffer::AppendBuffer(BigBuffer&& buffer) { - std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); - size_ += buffer.size_; - buffer.blocks_.clear(); - buffer.size_ = 0; -} - inline void BigBuffer::Pad(size_t bytes) { NextBlock<char>(bytes); } @@ -188,5 +174,3 @@ inline BigBuffer::const_iterator BigBuffer::end() const { } } // namespace android - -#endif // _ANDROID_BIG_BUFFER_H diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp index 382d21e20846..7e38f1758057 100644 --- a/libs/androidfw/tests/BigBuffer_test.cpp +++ b/libs/androidfw/tests/BigBuffer_test.cpp @@ -98,4 +98,20 @@ TEST(BigBufferTest, PadAndAlignProperly) { ASSERT_EQ(8u, buffer.size()); } +TEST(BigBufferTest, BackUpZeroed) { + BigBuffer buffer(16); + + auto block = buffer.NextBlock<char>(2); + ASSERT_TRUE(block != nullptr); + ASSERT_EQ(2u, buffer.size()); + block[0] = 0x01; + block[1] = 0x02; + buffer.BackUp(1); + ASSERT_EQ(1u, buffer.size()); + auto new_block = buffer.NextBlock<char>(1); + ASSERT_TRUE(new_block != nullptr); + ASSERT_EQ(2u, buffer.size()); + ASSERT_EQ(0, *new_block); +} + } // namespace android diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING index b3fb1e7b3034..ff836104f799 100644 --- a/packages/PackageInstaller/TEST_MAPPING +++ b/packages/PackageInstaller/TEST_MAPPING @@ -28,6 +28,17 @@ }, { "name": "CtsIntentSignatureTestCases" + }, + { + "name": "CtsPackageInstallerCUJTestCases", + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + } + ] } ] } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java index 492828d701b9..64e503b323b8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java @@ -174,7 +174,6 @@ public class ZenModesBackend { mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG, /* fromUser= */ true); } else { - // TODO: b/333527800 - This should (potentially) snooze the rule if it was active. mNotificationManager.setAutomaticZenRuleState(mode.getId(), new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE, Condition.SOURCE_USER_ACTION)); diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index 69c7410818dd..6198d80cefe6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -181,14 +181,14 @@ public class CreateUserDialogController { * admin status. */ public Dialog createDialog(Activity activity, - ActivityStarter activityStarter, boolean isMultipleAdminEnabled, + ActivityStarter activityStarter, boolean canCreateAdminUser, NewUserData successCallback, Runnable cancelCallback) { mActivity = activity; mCustomDialogHelper = new CustomDialogHelper(activity); mSuccessCallback = successCallback; mCancelCallback = cancelCallback; mActivityStarter = activityStarter; - addCustomViews(isMultipleAdminEnabled); + addCustomViews(canCreateAdminUser); mUserCreationDialog = mCustomDialogHelper.getDialog(); updateLayout(); mUserCreationDialog.setOnDismissListener(view -> finish()); @@ -197,19 +197,19 @@ public class CreateUserDialogController { return mUserCreationDialog; } - private void addCustomViews(boolean isMultipleAdminEnabled) { + private void addCustomViews(boolean canCreateAdminUser) { addGrantAdminView(); addUserInfoEditView(); mCustomDialogHelper.setPositiveButton(R.string.next, view -> { mCurrentState++; - if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + if (mCurrentState == GRANT_ADMIN_DIALOG && !canCreateAdminUser) { mCurrentState++; } updateLayout(); }); mCustomDialogHelper.setNegativeButton(R.string.back, view -> { mCurrentState--; - if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + if (mCurrentState == GRANT_ADMIN_DIALOG && !canCreateAdminUser) { mCurrentState--; } updateLayout(); diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index c88c4c94b5fb..0e71116db6cc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -25,6 +25,7 @@ import android.media.AudioManager.OnCommunicationDeviceChangedListener import android.provider.Settings import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils +import com.android.settingslib.volume.shared.AudioLogger import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent import com.android.settingslib.volume.shared.model.AudioStream @@ -99,7 +100,7 @@ class AudioRepositoryImpl( private val contentResolver: ContentResolver, private val backgroundCoroutineContext: CoroutineContext, private val coroutineScope: CoroutineScope, - private val logger: Logger, + private val logger: AudioLogger, ) : AudioRepository { private val streamSettingNames: Map<AudioStream, String> = @@ -117,10 +118,10 @@ class AudioRepositoryImpl( override val mode: StateFlow<Int> = callbackFlow { - val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) } - audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) - awaitClose { audioManager.removeOnModeChangedListener(listener) } - } + val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) } + audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) + awaitClose { audioManager.removeOnModeChangedListener(listener) } + } .onStart { emit(audioManager.mode) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode) @@ -140,14 +141,14 @@ class AudioRepositoryImpl( override val communicationDevice: StateFlow<AudioDeviceInfo?> get() = callbackFlow { - val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } - audioManager.addOnCommunicationDeviceChangedListener( - ConcurrentUtils.DIRECT_EXECUTOR, - listener, - ) + val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } + audioManager.addOnCommunicationDeviceChangedListener( + ConcurrentUtils.DIRECT_EXECUTOR, + listener, + ) - awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } - } + awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } + } .filterNotNull() .map { audioManager.communicationDevice } .onStart { emit(audioManager.communicationDevice) } @@ -160,15 +161,15 @@ class AudioRepositoryImpl( override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { return merge( - audioManagerEventsReceiver.events.filter { - if (it is StreamAudioManagerEvent) { - it.audioStream == audioStream - } else { - true - } - }, - volumeSettingChanges(audioStream), - ) + audioManagerEventsReceiver.events.filter { + if (it is StreamAudioManagerEvent) { + it.audioStream == audioStream + } else { + true + } + }, + volumeSettingChanges(audioStream), + ) .conflate() .map { getCurrentAudioStream(audioStream) } .onStart { emit(getCurrentAudioStream(audioStream)) } @@ -251,11 +252,4 @@ class AudioRepositoryImpl( awaitClose { contentResolver.unregisterContentObserver(observer) } } } - - interface Logger { - - fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) - - fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel) - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt index 7a66335ef22f..ebba7f152b90 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt @@ -34,6 +34,7 @@ import com.android.settingslib.bluetooth.onServiceStateChanged import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN +import com.android.settingslib.volume.shared.AudioSharingLogger import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.stateIn @@ -90,6 +92,7 @@ class AudioSharingRepositoryImpl( private val btManager: LocalBluetoothManager, private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, + private val logger: AudioSharingLogger ) : AudioSharingRepository { private val isAudioSharingProfilesReady: StateFlow<Boolean> = btManager.profileManager.onServiceStateChanged @@ -104,6 +107,7 @@ class AudioSharingRepositoryImpl( btManager.profileManager.leAudioBroadcastProfile.onBroadcastStartedOrStopped .map { isBroadcasting() } .onStart { emit(isBroadcasting()) } + .onEach { logger.onAudioSharingStateChanged(it) } .flowOn(backgroundCoroutineContext) } else { flowOf(false) @@ -156,6 +160,7 @@ class AudioSharingRepositoryImpl( .map { getSecondaryGroupId() }, primaryGroupId.map { getSecondaryGroupId() }) .onStart { emit(getSecondaryGroupId()) } + .onEach { logger.onSecondaryGroupIdChanged(it) } .flowOn(backgroundCoroutineContext) .stateIn( coroutineScope, @@ -202,6 +207,7 @@ class AudioSharingRepositoryImpl( acc } } + .onEach { logger.onVolumeMapChanged(it) } .flowOn(backgroundCoroutineContext) } else { emptyFlow() @@ -220,6 +226,7 @@ class AudioSharingRepositoryImpl( BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) if (cachedDevice != null) { it.setDeviceVolume(cachedDevice.device, volume, /* isGroupOp= */ true) + logger.onSetDeviceVolumeRequested(volume) } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioLogger.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioLogger.kt new file mode 100644 index 000000000000..84f7fcbd8b96 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioLogger.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.shared + +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.settingslib.volume.shared.model.AudioStreamModel + +/** A log interface for audio streams volume events. */ +interface AudioLogger { + fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) + + fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel) +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioSharingLogger.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioSharingLogger.kt new file mode 100644 index 000000000000..18a4c6d1748d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioSharingLogger.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.shared + +/** A log interface for audio sharing volume events. */ +interface AudioSharingLogger { + + fun onAudioSharingStateChanged(state: Boolean) + + fun onSecondaryGroupIdChanged(groupId: Int) + + fun onVolumeMapChanged(map: Map<Int, Int>) + + fun onSetDeviceVolumeRequested(volume: Int) +}
\ No newline at end of file diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt index 078f0c8adba5..8c5a0851cc92 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -101,7 +102,7 @@ class AudioSharingRepositoryTest { @Captor private lateinit var assistantCallbackCaptor: - ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback> + ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback> @Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback> @@ -110,6 +111,7 @@ class AudioSharingRepositoryTest { @Captor private lateinit var volumeCallbackCaptor: ArgumentCaptor<BluetoothVolumeControl.Callback> + private val logger = FakeAudioSharingRepositoryLogger() private val testScope = TestScope() private val context: Context = ApplicationProvider.getApplicationContext() @Spy private val contentResolver: ContentResolver = context.contentResolver @@ -135,16 +137,23 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID_INVALID) + TEST_GROUP_ID_INVALID + ) underTest = AudioSharingRepositoryImpl( contentResolver, btManager, testScope.backgroundScope, testScope.testScheduler, + logger ) } + @After + fun tearDown() { + logger.reset() + } + @Test fun audioSharingStateChange_profileReady_emitValues() { testScope.runTest { @@ -160,6 +169,13 @@ class AudioSharingRepositoryTest { runCurrent() Truth.assertThat(states).containsExactly(false, true, false, true) + Truth.assertThat(logger.logs) + .containsAtLeastElementsIn( + listOf( + "onAudioSharingStateChanged state=true", + "onAudioSharingStateChanged state=false", + ) + ).inOrder() } } @@ -187,7 +203,8 @@ class AudioSharingRepositoryTest { Truth.assertThat(groupIds) .containsExactly( TEST_GROUP_ID_INVALID, - TEST_GROUP_ID2) + TEST_GROUP_ID2 + ) } } @@ -219,13 +236,16 @@ class AudioSharingRepositoryTest { triggerSourceAdded() runCurrent() triggerProfileConnectionChange( - BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + ) runCurrent() triggerProfileConnectionChange( - BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO) + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO + ) runCurrent() triggerProfileConnectionChange( - BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + ) runCurrent() Truth.assertThat(groupIds) @@ -235,7 +255,16 @@ class AudioSharingRepositoryTest { TEST_GROUP_ID1, TEST_GROUP_ID_INVALID, TEST_GROUP_ID2, - TEST_GROUP_ID_INVALID) + TEST_GROUP_ID_INVALID + ) + Truth.assertThat(logger.logs) + .containsAtLeastElementsIn( + listOf( + "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID_INVALID", + "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID2", + "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID1", + ) + ).inOrder() } } @@ -257,11 +286,22 @@ class AudioSharingRepositoryTest { verify(volumeControl).unregisterCallback(any()) runCurrent() + val expectedMap1 = mapOf(TEST_GROUP_ID1 to TEST_VOLUME1) + val expectedMap2 = mapOf(TEST_GROUP_ID1 to TEST_VOLUME2) Truth.assertThat(volumeMaps) .containsExactly( emptyMap<Int, Int>(), - mapOf(TEST_GROUP_ID1 to TEST_VOLUME1), - mapOf(TEST_GROUP_ID1 to TEST_VOLUME2)) + expectedMap1, + expectedMap2 + ) + Truth.assertThat(logger.logs) + .containsAtLeastElementsIn( + listOf( + "onVolumeMapChanged map={}", + "onVolumeMapChanged map=$expectedMap1", + "onVolumeMapChanged map=$expectedMap2", + ) + ).inOrder() } } @@ -281,12 +321,19 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID2) + TEST_GROUP_ID2 + ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) underTest.setSecondaryVolume(TEST_VOLUME1) runCurrent() verify(volumeControl).setDeviceVolume(device1, TEST_VOLUME1, true) + Truth.assertThat(logger.logs) + .isEqualTo( + listOf( + "onSetVolumeRequested volume=$TEST_VOLUME1", + ) + ) } } @@ -313,7 +360,8 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID1) + TEST_GROUP_ID1 + ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) assistantCallbackCaptor.value.sourceAdded(device1, receiveState) } @@ -324,7 +372,8 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID1) + TEST_GROUP_ID1 + ) assistantCallbackCaptor.value.sourceRemoved(device2) } @@ -334,7 +383,8 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID1) + TEST_GROUP_ID1 + ) btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile) } @@ -343,12 +393,14 @@ class AudioSharingRepositoryTest { .registerContentObserver( eq(Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast())), eq(false), - contentObserverCaptor.capture()) + contentObserverCaptor.capture() + ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID2) + TEST_GROUP_ID2 + ) contentObserverCaptor.value.primaryChanged() } @@ -380,8 +432,9 @@ class AudioSharingRepositoryTest { onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID) } val sourceAdded: - BluetoothLeBroadcastAssistant.Callback.( - sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState) -> Unit = + BluetoothLeBroadcastAssistant.Callback.( + sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState + ) -> Unit = { sink, state -> onReceiveStateChanged(sink, TEST_SOURCE_ID, state) } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt index 389bf5304262..bd573fb2c675 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt @@ -16,10 +16,11 @@ package com.android.settingslib.volume.data.repository +import com.android.settingslib.volume.shared.AudioLogger import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel -class FakeAudioRepositoryLogger : AudioRepositoryImpl.Logger { +class FakeAudioRepositoryLogger : AudioLogger { private val mutableLogs: MutableList<String> = mutableListOf() val logs: List<String> diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioSharingRepositoryLogger.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioSharingRepositoryLogger.kt new file mode 100644 index 000000000000..cc4cc8d4ab96 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioSharingRepositoryLogger.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.data.repository + +import com.android.settingslib.volume.shared.AudioSharingLogger +import java.util.concurrent.CopyOnWriteArrayList + +class FakeAudioSharingRepositoryLogger : AudioSharingLogger { + private val mutableLogs = CopyOnWriteArrayList<String>() + val logs: List<String> + get() = mutableLogs.toList() + + fun reset() { + mutableLogs.clear() + } + + override fun onAudioSharingStateChanged(state: Boolean) { + mutableLogs.add("onAudioSharingStateChanged state=$state") + } + + override fun onSecondaryGroupIdChanged(groupId: Int) { + mutableLogs.add("onSecondaryGroupIdChanged groupId=$groupId") + } + + override fun onVolumeMapChanged(map: GroupIdToVolumes) { + mutableLogs.add("onVolumeMapChanged map=$map") + } + + override fun onSetDeviceVolumeRequested(volume: Int) { + mutableLogs.add("onSetVolumeRequested volume=$volume") + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java index 00c7ae3e97d7..539519b3ec3b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java @@ -123,7 +123,6 @@ public class ZenModesBackendTest { zenRule.id = id; zenRule.pkg = "package"; zenRule.enabled = azr.isEnabled(); - zenRule.snoozing = false; zenRule.conditionId = azr.getConditionId(); zenRule.condition = new Condition(azr.getConditionId(), "", active ? Condition.STATE_TRUE : Condition.STATE_FALSE, diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 9f3c2bfb237a..1d9f46971502 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -482,6 +482,15 @@ android:exported="true" android:theme="@style/Theme.AppCompat.NoActionBar"> <intent-filter> + <action android:name="com.android.systemui.action.TOUCHPAD_TUTORIAL"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> + + <activity android:name=".inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity" + android:exported="true" + android:theme="@style/Theme.AppCompat.NoActionBar"> + <intent-filter> <action android:name="com.android.systemui.action.TOUCHPAD_KEYBOARD_TUTORIAL"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java index 2e036e651d4e..6bea30fa8d08 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -348,7 +348,17 @@ public class A11yMenuOverlayLayout { /** Toggles a11y menu layout visibility. */ public void toggleVisibility() { - mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE); + if (mLayout.getVisibility() == View.VISIBLE) { + mLayout.setVisibility(View.GONE); + } else { + if (Flags.hideRestrictedActions()) { + // Reconfigure the shortcut list in case the set of restricted actions has changed. + mA11yMenuViewPager.configureViewPagerAndFooter( + mLayout, createShortcutList(), getPageIndex()); + updateViewLayout(); + } + mLayout.setVisibility(View.VISIBLE); + } } /** Shows hint text on a minimal Snackbar-like text view. */ diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index d16617fdc8e5..4ab771be55f4 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -542,8 +542,6 @@ public class AccessibilityMenuServiceTest { final Context context = sInstrumentation.getTargetContext(); final UserManager userManager = context.getSystemService(UserManager.class); userManager.setUserRestriction(restriction, isRestricted); - // Re-enable the service for the restriction to take effect. - enableA11yMenuService(context); } private static void unlockSignal() throws IOException { diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig index e81d5d5ece5a..95e4b593a72f 100644 --- a/packages/SystemUI/aconfig/biometrics_framework.aconfig +++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig @@ -3,9 +3,3 @@ container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. -flag { - name: "constraint_bp" - namespace: "biometrics_framework" - description: "Refactors Biometric Prompt to use a ConstraintLayout" - bug: "288175072" -} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 9046d4e328f4..03149928249b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1237,6 +1237,16 @@ flag { } flag { + name: "relock_with_power_button_immediately" + namespace: "systemui" + description: "UDFPS unlock followed by immediate power button push should relock" + bug: "343327511" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "lockscreen_preview_renderer_create_on_main_thread" namespace: "systemui" description: "Force preview renderer to be created on the main thread" @@ -1263,6 +1273,13 @@ flag { } flag { + name: "new_picker_ui" + namespace: "systemui" + description: "Enables the BC25 design of the customization picker UI." + bug: "339081035" +} + +flag { namespace: "systemui" name: "settings_ext_register_content_observer_on_bg_thread" description: "Register content observer in callback flow APIs on background thread in SettingsProxyExt." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt new file mode 100644 index 000000000000..04bcc3624532 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.composable + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.modifiers.background +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.deviceentry.shared.model.BiometricMessage +import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder +import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay +import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel +import com.android.systemui.keyguard.ui.binder.AlternateBouncerUdfpsViewBinder +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel +import com.android.systemui.res.R +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +@Composable +fun AlternateBouncer( + alternateBouncerDependencies: AlternateBouncerDependencies, + modifier: Modifier = Modifier, +) { + + val isVisible by + alternateBouncerDependencies.viewModel.isVisible.collectAsStateWithLifecycle( + initialValue = false + ) + + val udfpsIconLocation by + alternateBouncerDependencies.udfpsIconViewModel.iconLocation.collectAsStateWithLifecycle( + initialValue = null + ) + + // TODO (b/353955910): back handling doesn't work + BackHandler { alternateBouncerDependencies.viewModel.onBackRequested() } + + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + ) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = + Modifier.background(color = Colors.AlternateBouncerBackgroundColor, alpha = { 1f }) + .pointerInput(Unit) { + detectTapGestures( + onTap = { alternateBouncerDependencies.viewModel.onTapped() } + ) + }, + ) { + StatusMessage( + viewModel = alternateBouncerDependencies.messageAreaViewModel, + ) + } + + udfpsIconLocation?.let { udfpsLocation -> + Box { + DeviceEntryIcon( + viewModel = alternateBouncerDependencies.udfpsIconViewModel, + modifier = + Modifier.width { udfpsLocation.width } + .height { udfpsLocation.height } + .fillMaxHeight() + .offset { + IntOffset( + x = udfpsLocation.left, + y = udfpsLocation.top, + ) + }, + ) + } + + UdfpsA11yOverlay( + viewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel.get(), + modifier = Modifier.fillMaxHeight(), + ) + } + } +} + +@ExperimentalCoroutinesApi +@Composable +private fun StatusMessage( + viewModel: AlternateBouncerMessageAreaViewModel, + modifier: Modifier = Modifier, +) { + val message: BiometricMessage? by + viewModel.message.collectAsStateWithLifecycle(initialValue = null) + + Crossfade( + targetState = message, + label = "Alternate Bouncer message", + animationSpec = tween(), + modifier = modifier, + ) { biometricMessage -> + biometricMessage?.let { + Text( + textAlign = TextAlign.Center, + text = it.message ?: "", + color = Colors.AlternateBouncerTextColor, + fontSize = 18.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 92.dp), + ) + } + } +} + +@ExperimentalCoroutinesApi +@Composable +private fun DeviceEntryIcon( + viewModel: AlternateBouncerUdfpsIconViewModel, + modifier: Modifier = Modifier, +) { + AndroidView( + modifier = modifier, + factory = { context -> + val view = + DeviceEntryIconView(context, null).apply { + id = R.id.alternate_bouncer_udfps_icon_view + contentDescription = + context.resources.getString(R.string.accessibility_fingerprint_label) + } + AlternateBouncerUdfpsViewBinder.bind(view, viewModel) + view + }, + ) +} + +/** TODO (b/353955910): Validate accessibility CUJs */ +@ExperimentalCoroutinesApi +@Composable +private fun UdfpsA11yOverlay( + viewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel, + modifier: Modifier = Modifier, +) { + AndroidView( + factory = { context -> + val view = + UdfpsAccessibilityOverlay(context).apply { + id = R.id.alternate_bouncer_udfps_accessibility_overlay + } + UdfpsAccessibilityOverlayBinder.bind(view, viewModel) + view + }, + modifier = modifier, + ) +} + +private object Colors { + val AlternateBouncerBackgroundColor: Color = Color.Black.copy(alpha = .66f) + val AlternateBouncerTextColor: Color = Color.White +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt index 26ab10b459d8..85d5d6a41965 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt @@ -24,9 +24,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.MovableElementKey import com.android.compose.animation.scene.SceneScope @@ -51,6 +53,7 @@ fun SceneScope.MediaCarousel( mediaHost: MediaHost, modifier: Modifier = Modifier, carouselController: MediaCarouselController, + offsetProvider: (() -> IntOffset)? = null, ) { if (!isVisible) { return @@ -68,21 +71,38 @@ fun SceneScope.MediaCarousel( MovableElement( key = MediaCarousel.Elements.Content, - modifier = modifier.height(mediaHeight).fillMaxWidth() + modifier = modifier.height(mediaHeight).fillMaxWidth(), ) { content { AndroidView( modifier = - Modifier.fillMaxSize().layout { measurable, constraints -> - val placeable = measurable.measure(constraints) + Modifier.fillMaxSize() + .approachLayout( + isMeasurementApproachInProgress = { offsetProvider != null }, + approachMeasure = { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.placeRelative( + offsetProvider?.invoke() ?: IntOffset.Zero + ) + } + } + ) + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) - // Notify controller to size the carousel for the current space - mediaHost.measurementInput = - MeasurementInput(placeable.width, placeable.height) - carouselController.setSceneContainerSize(placeable.width, placeable.height) + // Notify controller to size the carousel for the current space + mediaHost.measurementInput = + MeasurementInput(placeable.width, placeable.height) + carouselController.setSceneContainerSize( + placeable.width, + placeable.height + ) - layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } - }, + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + }, factory = { context -> FrameLayout(context).apply { layoutParams = diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt index 3f04f3728bef..70c0db1582c4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaContentPicker.kt @@ -27,7 +27,6 @@ import com.android.systemui.scene.shared.model.Scenes /** [ElementContentPicker] implementation for the media carousel object. */ object MediaContentPicker : StaticElementContentPicker { - const val SHADE_FRACTION = 0.66f override val contents = setOf( Scenes.Lockscreen, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt index a9da733116fc..71fa6c9e567a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt @@ -25,7 +25,6 @@ import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.media.controls.ui.composable.MediaCarousel -import com.android.systemui.media.controls.ui.composable.MediaContentPicker import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.shade.ui.composable.Shade @@ -54,13 +53,7 @@ fun TransitionBuilder.goneToSplitShadeTransition( fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) } fractionRange(start = .33f) { - val qsTranslation = - ShadeHeader.Dimensions.CollapsedHeight * MediaContentPicker.SHADE_FRACTION - val qsExpansionDiff = - ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight - translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation)) fade(MediaCarousel.Elements.Content) - fade(ShadeHeader.Elements.Clock) fade(ShadeHeader.Elements.CollapsedContentStart) fade(ShadeHeader.Elements.CollapsedContentEnd) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt index 21dfc49cbe7b..b677dff2dcf9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt @@ -26,7 +26,6 @@ import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.media.controls.ui.composable.MediaCarousel -import com.android.systemui.media.controls.ui.composable.MediaContentPicker import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes @@ -62,12 +61,9 @@ fun TransitionBuilder.toShadeTransition( fade(QuickSettings.Elements.FooterActions) } - val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaContentPicker.SHADE_FRACTION - val qsExpansionDiff = - ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight - - translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation) - translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation)) + val qsTranslation = -ShadeHeader.Dimensions.CollapsedHeight * 0.66f + translate(QuickSettings.Elements.QuickQuickSettings, y = qsTranslation) + translate(MediaCarousel.Elements.Content, y = qsTranslation) translate(Notifications.Elements.NotificationScrim, Edge.Top, false) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeMediaOffsetProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeMediaOffsetProvider.kt new file mode 100644 index 000000000000..e0b7909dcaa3 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeMediaOffsetProvider.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.composable + +import androidx.compose.ui.unit.IntOffset +import com.android.systemui.qs.ui.adapter.QSSceneAdapter + +/** + * Provider for the extra offset for the Media section in the shade to accommodate for the squishing + * qs or qqs tiles. + */ +interface ShadeMediaOffsetProvider { + + /** Returns current offset to be applied to the Media Carousel */ + val offset: IntOffset + + /** + * [ShadeMediaOffsetProvider] implementation for Quick Settings. + * + * [updateLayout] should represent an access to some state to trigger Compose to relayout to + * track [QSSceneAdapter] internal state changes during the transition. + */ + class Qs(private val updateLayout: () -> Unit, private val qsSceneAdapter: QSSceneAdapter) : + ShadeMediaOffsetProvider { + + override val offset: IntOffset + get() = + calculateQsOffset( + updateLayout, + qsSceneAdapter.qsHeight, + qsSceneAdapter.squishedQsHeight + ) + } + + /** + * [ShadeMediaOffsetProvider] implementation for Quick Quick Settings. + * + * [updateLayout] should represent an access to some state to trigger Compose to relayout to + * track [QSSceneAdapter] internal state changes during the transition. + */ + class Qqs(private val updateLayout: () -> Unit, private val qsSceneAdapter: QSSceneAdapter) : + ShadeMediaOffsetProvider { + + override val offset: IntOffset + get() = + calculateQsOffset( + updateLayout, + qsSceneAdapter.qqsHeight, + qsSceneAdapter.squishedQqsHeight + ) + } + + companion object { + + protected fun calculateQsOffset( + updateLayout: () -> Unit, + qsHeight: Int, + qsSquishedHeight: Int + ): IntOffset { + updateLayout() + val distanceFromBottomToActualBottom = qsHeight - qsSquishedHeight + return IntOffset(0, -distanceFromBottomToActualBottom) + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 0e3fcf4598af..16972bc95e57 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -118,7 +118,6 @@ import kotlinx.coroutines.flow.Flow object Shade { object Elements { - val MediaCarousel = ElementKey("ShadeMediaCarousel") val BackgroundScrim = ElementKey("ShadeBackgroundScrim", contentPicker = LowestZIndexContentPicker) val SplitShadeStartColumn = ElementKey("SplitShadeStartColumn") @@ -283,6 +282,13 @@ private fun SceneScope.SingleShade( val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + val mediaOffsetProvider = remember { + ShadeMediaOffsetProvider.Qqs( + { @Suppress("UNUSED_EXPRESSION") tileSquishiness }, + viewModel.qsSceneAdapter, + ) + } + Box( modifier = modifier.thenIf(shouldPunchHoleBehindScrim) { @@ -335,12 +341,12 @@ private fun SceneScope.SingleShade( ) } - MediaCarousel( + ShadeMediaCarousel( isVisible = isMediaVisible, mediaHost = mediaHost, + mediaOffsetProvider = mediaOffsetProvider, modifier = - Modifier.fillMaxWidth() - .layoutId(QSMediaMeasurePolicy.LayoutId.Media), + Modifier.layoutId(QSMediaMeasurePolicy.LayoutId.Media), carouselController = mediaCarouselController, ) } @@ -497,6 +503,13 @@ private fun SceneScope.SplitShade( val brightnessMirrorShowingModifier = Modifier.graphicsLayer { alpha = contentAlpha } + val mediaOffsetProvider = remember { + ShadeMediaOffsetProvider.Qs( + { @Suppress("UNUSED_EXPRESSION") tileSquishiness }, + viewModel.qsSceneAdapter, + ) + } + Box { Box( modifier = @@ -570,11 +583,13 @@ private fun SceneScope.SplitShade( squishiness = { tileSquishiness }, ) } - MediaCarousel( + + ShadeMediaCarousel( isVisible = isMediaVisible, mediaHost = mediaHost, + mediaOffsetProvider = mediaOffsetProvider, modifier = - Modifier.fillMaxWidth().thenIf( + Modifier.thenIf( MediaContentPicker.shouldElevateMedia(layoutState) ) { Modifier.zIndex(1f) @@ -620,3 +635,25 @@ private fun SceneScope.SplitShade( ) } } + +@Composable +private fun SceneScope.ShadeMediaCarousel( + isVisible: Boolean, + mediaHost: MediaHost, + carouselController: MediaCarouselController, + mediaOffsetProvider: ShadeMediaOffsetProvider, + modifier: Modifier = Modifier, +) { + MediaCarousel( + modifier = modifier.fillMaxWidth(), + isVisible = isVisible, + mediaHost = mediaHost, + carouselController = carouselController, + offsetProvider = + if (MediaContentPicker.shouldElevateMedia(layoutState)) { + null + } else { + { mediaOffsetProvider.offset } + } + ) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index 9da2a1b06f30..5ffb6f82fbba 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -50,7 +50,7 @@ fun VolumePanelRoot( ) { val accessibilityTitle = stringResource(R.string.accessibility_volume_settings) val state: VolumePanelState by viewModel.volumePanelState.collectAsStateWithLifecycle() - val components by viewModel.componentsLayout.collectAsStateWithLifecycle(null) + val components by viewModel.componentsLayout.collectAsStateWithLifecycle() with(VolumePanelComposeScope(state)) { components?.let { componentsState -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index a43028a340f4..712fe6b2ff50 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -459,38 +459,11 @@ private class DragControllerImpl( animateTo(targetScene = targetScene, targetOffset = targetOffset) } else { - // We are doing an overscroll animation between scenes. In this case, we can also start - // from the idle position. - - val startFromIdlePosition = swipeTransition.dragOffset == 0f - - if (startFromIdlePosition) { - // If there is a target scene, we start the overscroll animation. - val result = swipes.findUserActionResultStrict(velocity) - if (result == null) { - // We will not animate - swipeTransition.snapToScene(fromScene.key) - return 0f - } - - val newSwipeTransition = - SwipeTransition( - layoutState = layoutState, - coroutineScope = draggableHandler.coroutineScope, - fromScene = fromScene, - result = result, - swipes = swipes, - layoutImpl = draggableHandler.layoutImpl, - orientation = draggableHandler.orientation, - ) - .apply { _currentScene = swipeTransition._currentScene } - - updateTransition(newSwipeTransition) - animateTo(targetScene = fromScene, targetOffset = 0f) - } else { - // We were between two scenes: animate to the initial scene. - animateTo(targetScene = fromScene, targetOffset = 0f) + // We are doing an overscroll preview animation between scenes. + check(fromScene == swipeTransition._currentScene) { + "canChangeScene is false but currentScene != fromScene" } + animateTo(targetScene = fromScene, targetOffset = 0f) } // The onStop animation consumes any remaining velocity. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index a30b78049213..79b38563b8f9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -17,6 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset @@ -140,6 +142,7 @@ interface BaseTransitionBuilder : PropertyTransformationBuilder { fun fractionRange( start: Float? = null, end: Float? = null, + easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) } @@ -182,6 +185,7 @@ interface TransitionBuilder : BaseTransitionBuilder { fun timestampRange( startMillis: Int? = null, endMillis: Int? = null, + easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 6515cb8f68ca..a63b19a0306f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -18,6 +18,7 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.DurationBasedAnimationSpec +import androidx.compose.animation.core.Easing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.VectorConverter @@ -163,9 +164,10 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { override fun fractionRange( start: Float?, end: Float?, + easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { - range = TransformationRange(start, end) + range = TransformationRange(start, end, easing) builder() range = null } @@ -251,6 +253,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu override fun timestampRange( startMillis: Int?, endMillis: Int?, + easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) { @@ -263,7 +266,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu val start = startMillis?.let { it.toFloat() / durationMillis } val end = endMillis?.let { it.toFloat() / durationMillis } - fractionRange(start, end, builder) + fractionRange(start, end, easing, builder) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 77ec89161d43..eda8edeceeb9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -16,6 +16,8 @@ package com.android.compose.animation.scene.transformation +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.LinearEasing import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastCoerceIn @@ -90,11 +92,13 @@ internal class RangedPropertyTransformation<T>( data class TransformationRange( val start: Float, val end: Float, + val easing: Easing, ) { constructor( start: Float? = null, - end: Float? = null - ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified) + end: Float? = null, + easing: Easing = LinearEasing, + ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified, easing) init { require(!start.isSpecified() || (start in 0f..1f)) @@ -103,17 +107,20 @@ data class TransformationRange( } /** Reverse this range. */ - fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start)) + fun reversed() = + TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing) /** Get the progress of this range given the global [transitionProgress]. */ fun progress(transitionProgress: Float): Float { - return when { - start.isSpecified() && end.isSpecified() -> - ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f) - !start.isSpecified() && !end.isSpecified() -> transitionProgress - end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f) - else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f) - } + val progress = + when { + start.isSpecified() && end.isSpecified() -> + ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f) + !start.isSpecified() && !end.isSpecified() -> transitionProgress + end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f) + else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f) + } + return easing.transform(progress) } private fun Float.isSpecified() = this != BoundUnspecified diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index 68240b5337fe..bed6cefa459d 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.spring @@ -107,6 +108,13 @@ class TransitionDslTest { fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } fractionRange(start = 0.2f) { fade(TestElements.Foo) } fractionRange(end = 0.9f) { fade(TestElements.Foo) } + fractionRange( + start = 0.1f, + end = 0.8f, + easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ) { + fade(TestElements.Foo) + } } } @@ -118,6 +126,11 @@ class TransitionDslTest { TransformationRange(start = 0.1f, end = 0.8f), TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f), + TransformationRange( + start = 0.1f, + end = 0.8f, + CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ), ) } @@ -130,6 +143,13 @@ class TransitionDslTest { timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } timestampRange(startMillis = 200) { fade(TestElements.Foo) } timestampRange(endMillis = 400) { fade(TestElements.Foo) } + timestampRange( + startMillis = 100, + endMillis = 300, + easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ) { + fade(TestElements.Foo) + } } } @@ -141,6 +161,11 @@ class TransitionDslTest { TransformationRange(start = 100 / 500f, end = 300 / 500f), TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f), + TransformationRange( + start = 100 / 500f, + end = 300 / 500f, + easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ), ) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt new file mode 100644 index 000000000000..07901f27388d --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt @@ -0,0 +1,126 @@ +/* + * 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.compose.animation.scene.transformation + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestElements +import com.android.compose.animation.scene.testTransition +import com.android.compose.test.assertSizeIsEqualTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EasingTest { + @get:Rule val rule = createComposeRule() + + @Test + fun testFractionRangeEasing() { + val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + rule.testTransition( + fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, + toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, + transition = { + // Scale during 4 frames. + spec = tween(16 * 4, easing = LinearEasing) + fractionRange(easing = easing) { + scaleSize(TestElements.Foo, width = 0f, height = 0f) + scaleSize(TestElements.Bar, width = 0f, height = 0f) + } + }, + ) { + // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the + // transition so it starts at 200dp x 50dp. + before { onElement(TestElements.Bar).assertDoesNotExist() } + at(0) { + onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) + } + at(16) { + // 25% linear progress is mapped to 68.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) + } + at(32) { + // 50% linear progress is mapped to 89.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) + } + at(48) { + // 75% linear progress is mapped to 97.8% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) + } + after { + onElement(TestElements.Foo).assertDoesNotExist() + onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) + } + } + } + + @Test + fun testTimestampRangeEasing() { + val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + rule.testTransition( + fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, + toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, + transition = { + // Scale during 4 frames. + spec = tween(16 * 4, easing = LinearEasing) + timestampRange(easing = easing) { + scaleSize(TestElements.Foo, width = 0f, height = 0f) + scaleSize(TestElements.Bar, width = 0f, height = 0f) + } + }, + ) { + // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the + // transition so it starts at 200dp x 50dp. + before { onElement(TestElements.Bar).assertDoesNotExist() } + at(0) { + onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) + } + at(16) { + // 25% linear progress is mapped to 68.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) + } + at(32) { + // 50% linear progress is mapped to 89.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) + } + at(48) { + // 75% linear progress is mapped to 97.8% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) + } + after { + onElement(TestElements.Foo).assertDoesNotExist() + onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index 9b1d4eca8b2b..752c93e077ac 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -148,8 +148,6 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private WakefulnessLifecycle mWakefulnessLifecycle; @Mock - private AuthDialogPanelInteractionDetector mPanelInteractionDetector; - @Mock private UserManager mUserManager; @Mock private LockPatternUtils mLockPatternUtils; @@ -1059,10 +1057,9 @@ public class AuthControllerTest extends SysuiTestCase { super(context, null /* applicationCoroutineScope */, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager, () -> mUdfpsController, mDisplayManager, - mWakefulnessLifecycle, mPanelInteractionDetector, mUserManager, - mLockPatternUtils, () -> mUdfpsLogger, () -> mLogContextInteractor, - () -> mPromptSelectionInteractor, () -> mCredentialViewModel, - () -> mPromptViewModel, mInteractionJankMonitor, + mWakefulnessLifecycle, mUserManager, mLockPatternUtils, () -> mUdfpsLogger, + () -> mLogContextInteractor, () -> mPromptSelectionInteractor, + () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper); } @@ -1071,7 +1068,6 @@ public class AuthControllerTest extends SysuiTestCase { boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, WakefulnessLifecycle wakefulnessLifecycle, - AuthDialogPanelInteractionDetector panelInteractionDetector, UserManager userManager, LockPatternUtils lockPatternUtils, PromptViewModel viewModel) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java deleted file mode 100644 index cd9189bef7f1..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static org.junit.Assert.assertEquals; - -import android.hardware.biometrics.ComponentInfoInternal; -import android.hardware.biometrics.SensorLocationInternal; -import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class UdfpsDialogMeasureAdapterTest extends SysuiTestCase { - @Test - public void testUdfpsBottomSpacerHeightForPortrait() { - final int displayHeightPx = 3000; - final int navbarHeightPx = 10; - final int dialogBottomMarginPx = 20; - final int buttonBarHeightPx = 100; - final int textIndicatorHeightPx = 200; - - final int sensorLocationX = 540; - final int sensorLocationY = 1600; - final int sensorRadius = 100; - - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - List.of(new SensorLocationInternal("" /* displayId */, - sensorLocationX, sensorLocationY, sensorRadius))); - - assertEquals(970, - UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForPortrait( - props, displayHeightPx, textIndicatorHeightPx, buttonBarHeightPx, - dialogBottomMarginPx, navbarHeightPx, 1.0f /* resolutionScale */ - )); - } - - @Test - public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceAboveIcon() { - final int titleHeightPx = 320; - final int subtitleHeightPx = 240; - final int descriptionHeightPx = 200; - final int topSpacerHeightPx = 550; - final int textIndicatorHeightPx = 190; - final int buttonBarHeightPx = 160; - final int navbarBottomInsetPx = 75; - - assertEquals(885, - UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape( - titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx, - textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx)); - } - - @Test - public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceBelowIcon() { - final int titleHeightPx = 315; - final int subtitleHeightPx = 160; - final int descriptionHeightPx = 75; - final int topSpacerHeightPx = 220; - final int textIndicatorHeightPx = 290; - final int buttonBarHeightPx = 360; - final int navbarBottomInsetPx = 205; - - assertEquals(-85, - UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape( - titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx, - textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx)); - } - - @Test - public void testUdfpsHorizontalSpacerWidthForLandscape() { - final int displayWidthPx = 3000; - final int dialogMarginPx = 20; - final int navbarHorizontalInsetPx = 75; - - final int sensorLocationX = 540; - final int sensorLocationY = 1600; - final int sensorRadius = 100; - - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - List.of(new SensorLocationInternal("" /* displayId */, - sensorLocationX, sensorLocationY, sensorRadius))); - - assertEquals(1205, - UdfpsDialogMeasureAdapter.calculateHorizontalSpacerWidthForLandscape( - props, displayWidthPx, dialogMarginPx, navbarHorizontalInsetPx, - 1.0f /* resolutionScale */)); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt index d1f908dfc795..46b370fedf37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt @@ -16,16 +16,27 @@ package com.android.systemui.lifecycle +import android.view.View import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.util.Assert import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -110,4 +121,45 @@ class SysUiViewModelTest : SysuiTestCase() { assertThat(isActive).isFalse() } + + @Test + fun viewModel_viewBinder() = runTest { + Assert.setTestThread(Thread.currentThread()) + + val view: View = mock { on { isAttachedToWindow } doReturn false } + val viewModel = FakeViewModel() + backgroundScope.launch { + view.viewModel( + minWindowLifecycleState = WindowLifecycleState.ATTACHED, + factory = { viewModel }, + ) { + awaitCancellation() + } + } + runCurrent() + + assertThat(viewModel.isActivated).isFalse() + + view.stub { on { isAttachedToWindow } doReturn true } + argumentCaptor<View.OnAttachStateChangeListener>() + .apply { verify(view).addOnAttachStateChangeListener(capture()) } + .allValues + .forEach { it.onViewAttachedToWindow(view) } + runCurrent() + + assertThat(viewModel.isActivated).isTrue() + } +} + +private class FakeViewModel : SysUiViewModel() { + var isActivated = false + + override suspend fun onActivated() { + isActivated = true + try { + awaitCancellation() + } finally { + isActivated = false + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt new file mode 100644 index 000000000000..5a73fe28ee18 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl + +import android.graphics.drawable.TestStubDrawable +import android.service.quicksettings.Tile +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper +import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel +import com.android.systemui.qs.tiles.impl.airplane.qsAirplaneModeTileConfig +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AirplaneModeMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val airplaneModeConfig = kosmos.qsAirplaneModeTileConfig + + private lateinit var mapper: AirplaneModeMapper + + @Before + fun setup() { + mapper = + AirplaneModeMapper( + context.orCreateTestableResources + .apply { + addOverride(R.drawable.qs_airplane_icon_off, TestStubDrawable()) + addOverride(R.drawable.qs_airplane_icon_on, TestStubDrawable()) + } + .resources, + context.theme, + ) + } + + @Test + fun enabledModel_mapsCorrectly() { + val inputModel = AirplaneModeTileModel(true) + + val outputState = mapper.map(airplaneModeConfig, inputModel) + + val expectedState = + createAirplaneModeState( + QSTileState.ActivationState.ACTIVE, + context.resources.getStringArray(R.array.tile_states_airplane)[Tile.STATE_ACTIVE], + R.drawable.qs_airplane_icon_on + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun disabledModel_mapsCorrectly() { + val inputModel = AirplaneModeTileModel(false) + + val outputState = mapper.map(airplaneModeConfig, inputModel) + + val expectedState = + createAirplaneModeState( + QSTileState.ActivationState.INACTIVE, + context.resources.getStringArray(R.array.tile_states_airplane)[Tile.STATE_INACTIVE], + R.drawable.qs_airplane_icon_off + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + private fun createAirplaneModeState( + activationState: QSTileState.ActivationState, + secondaryLabel: String, + iconRes: Int + ): QSTileState { + val label = context.getString(R.string.airplane_mode) + return QSTileState( + { Icon.Loaded(context.getDrawable(iconRes)!!, null) }, + iconRes, + label, + activationState, + secondaryLabel, + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), + label, + null, + QSTileState.SideViewIcon.None, + QSTileState.EnabledState.ENABLED, + Switch::class.qualifiedName + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt index b4ff56566c75..f1d08c068150 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.tiles.impl.custom.domain.interactor import android.app.IUriGrantsManager import android.content.ComponentName +import android.content.Context import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.graphics.drawable.TestStubDrawable @@ -51,11 +52,13 @@ import org.junit.runner.RunWith class CustomTileMapperTest : SysuiTestCase() { private val uriGrantsManager: IUriGrantsManager = mock {} + private val mockContext = + mock<Context> { whenever(createContextAsUser(any(), any())).thenReturn(context) } private val kosmos = testKosmos().apply { customTileSpec = TileSpec.Companion.create(TEST_COMPONENT) } private val underTest by lazy { CustomTileMapper( - context = mock { whenever(createContextAsUser(any(), any())).thenReturn(context) }, + context = mockContext, uriGrantsManager = uriGrantsManager, ) } @@ -164,7 +167,7 @@ class CustomTileMapperTest : SysuiTestCase() { ) val expected = createTileState( - activationState = QSTileState.ActivationState.INACTIVE, + activationState = QSTileState.ActivationState.UNAVAILABLE, icon = DEFAULT_DRAWABLE, ) @@ -173,7 +176,7 @@ class CustomTileMapperTest : SysuiTestCase() { } @Test - fun failedToLoadIconTileIsInactive() = + fun failedToLoadIconTileIsUnavailable() = with(kosmos) { testScope.runTest { val actual = @@ -187,13 +190,32 @@ class CustomTileMapperTest : SysuiTestCase() { val expected = createTileState( icon = null, - activationState = QSTileState.ActivationState.INACTIVE, + activationState = QSTileState.ActivationState.UNAVAILABLE, ) assertThat(actual).isEqualTo(expected) } } + @Test + fun nullUserContextDoesNotCauseExceptionReturnsNullIconAndUnavailableState() = + with(kosmos) { + testScope.runTest { + // map() will catch this exception + whenever(mockContext.createContextAsUser(any(), any())) + .thenThrow(IllegalStateException("Unable to create userContext")) + + val actual = underTest.map(customTileQsTileConfig, createModel()) + + val expected = + createTileState( + icon = null, + activationState = QSTileState.ActivationState.UNAVAILABLE, + ) + assertThat(actual).isEqualTo(expected) + } + } + private fun Kosmos.createModel( tileState: Int = Tile.STATE_ACTIVE, tileIcon: Icon = createIcon(DRAWABLE, false), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt index 13d6411382cf..1ea8abc9b3b3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt @@ -204,7 +204,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { val actualIcon = latest?.icon assertThat(actualIcon).isEqualTo(expectedIcon) - assertThat(latest?.iconId).isNull() + assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo("$internet,test ssid") val expectedSd = wifiIcon.contentDescription @@ -443,15 +443,15 @@ class InternetTileDataInteractorTest : SysuiTestCase() { * on the mentioned context. Since that context does not have a looper assigned to it, the * handler instantiation will throw a RuntimeException. * - * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception - * So either we should make Robolectric behvase similar to the device test, or change this - * test to look for a different signal than the exception, when run by Robolectric. For now - * we just assume the test is not Robolectric. + * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So + * either we should make Robolectric behave similar to the device test, or change this test to + * look for a different signal than the exception, when run by Robolectric. For now we just + * assume the test is not Robolectric. */ @Test(expected = java.lang.RuntimeException::class) fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() = testScope.runTest { - assumeFalse(isRobolectricTest()); + assumeFalse(isRobolectricTest()) collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt index ab184abdc963..f232d52615a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.volume.panel.domain.model.ComponentModel import com.android.systemui.volume.panel.domain.unavailableCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.ui.composable.enabledComponents +import com.android.systemui.volume.shared.volumePanelLogger import com.google.common.truth.Truth.assertThat import javax.inject.Provider import kotlinx.coroutines.test.runTest @@ -49,6 +50,7 @@ class ComponentsInteractorImplTest : SysuiTestCase() { enabledComponents, { defaultCriteria }, testScope.backgroundScope, + volumePanelLogger, criteriaByKey, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt index 420b955e88e0..51a70bda6034 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt @@ -24,21 +24,30 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dump.DumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos +import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory import com.android.systemui.volume.panel.data.repository.volumePanelGlobalStateRepository import com.android.systemui.volume.panel.domain.interactor.criteriaByKey +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor import com.android.systemui.volume.panel.domain.unavailableCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.shared.model.mockVolumePanelUiComponentProvider import com.android.systemui.volume.panel.ui.composable.componentByKey import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager import com.android.systemui.volume.panel.ui.layout.componentsLayoutManager +import com.android.systemui.volume.shared.volumePanelLogger import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -55,6 +64,7 @@ class VolumePanelViewModelTest : SysuiTestCase() { volumePanelGlobalStateRepository.updateVolumePanelState { it.copy(isVisible = true) } } + private val realDumpManager = DumpManager() private val testableResources = context.orCreateTestableResources private lateinit var underTest: VolumePanelViewModel @@ -124,6 +134,60 @@ class VolumePanelViewModelTest : SysuiTestCase() { } @Test + fun testDumpableRegister_unregister() = + with(kosmos) { + testScope.runTest { + val job = launch { + applicationCoroutineScope = this + underTest = createViewModel() + + runCurrent() + + assertThat(realDumpManager.getDumpables().any { it.name == DUMPABLE_NAME }) + .isTrue() + } + + runCurrent() + job.cancel() + + assertThat(realDumpManager.getDumpables().any { it.name == DUMPABLE_NAME }).isTrue() + } + } + + @Test + fun testDumpingState() = + test({ + componentByKey = + mapOf( + COMPONENT_1 to mockVolumePanelUiComponentProvider, + COMPONENT_2 to mockVolumePanelUiComponentProvider, + BOTTOM_BAR to mockVolumePanelUiComponentProvider, + ) + criteriaByKey = mapOf(COMPONENT_2 to Provider { unavailableCriteria }) + }) { + testScope.runTest { + runCurrent() + + StringWriter().use { + underTest.dump(PrintWriter(it), emptyArray()) + + assertThat(it.buffer.toString()) + .isEqualTo( + "volumePanelState=" + + "VolumePanelState(orientation=1, isLargeScreen=false)\n" + + "componentsLayout=( " + + "headerComponents= " + + "contentComponents=" + + "test_component:1:visible=true, " + + "test_component:2:visible=false " + + "footerComponents= " + + "bottomBarComponent=test_bottom_bar:visible=true )\n" + ) + } + } + } + + @Test fun dismissBroadcast_dismissesPanel() = test { testScope.runTest { runCurrent() // run the flows to let allow the receiver to be registered @@ -140,11 +204,26 @@ class VolumePanelViewModelTest : SysuiTestCase() { private fun test(setup: Kosmos.() -> Unit = {}, test: Kosmos.() -> Unit) = with(kosmos) { setup() - underTest = volumePanelViewModel + underTest = createViewModel() + test() } + private fun Kosmos.createViewModel(): VolumePanelViewModel = + VolumePanelViewModel( + context.orCreateTestableResources.resources, + applicationCoroutineScope, + volumePanelComponentFactory, + configurationController, + broadcastDispatcher, + realDumpManager, + volumePanelLogger, + volumePanelGlobalStateInteractor, + ) + private companion object { + const val DUMPABLE_NAME = "VolumePanelViewModel" + const val BOTTOM_BAR: VolumePanelComponentKey = "test_bottom_bar" const val COMPONENT_1: VolumePanelComponentKey = "test_component:1" const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml deleted file mode 100644 index ff89ed9e6e7a..000000000000 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ /dev/null @@ -1,208 +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. - --> -<com.android.systemui.biometrics.ui.BiometricPromptLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/contents" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <ImageView - android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" - android:layout_gravity="center" - android:scaleType="fitXY"/> - - <TextView - android:id="@+id/logo_description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" - android:marqueeRepeatLimit="1" - android:ellipsize="marquee"/> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" - android:marqueeRepeatLimit="1" - android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.OldTitle"/> - - <TextView - android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" - android:marqueeRepeatLimit="1" - android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.OldSubtitle"/> - - <TextView - android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:scrollbars ="vertical" - android:importantForAccessibility="no" - style="@style/TextAppearance.AuthCredential.OldDescription"/> - - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="24dp" - android:visibility="gone" /> - - <LinearLayout - android:id="@+id/customized_view_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fadeScrollbars="false" - android:gravity="center_vertical" - android:orientation="vertical" - android:scrollbars="vertical" - android:visibility="gone" /> - - <Space android:id="@+id/space_above_icon" - android:layout_width="match_parent" - android:layout_height="48dp" /> - - <FrameLayout - android:id="@+id/biometric_icon_frame" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center"> - - <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper - android:id="@+id/biometric_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:contentDescription="@null" - android:scaleType="fitXY" /> - - <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper - android:id="@+id/biometric_icon_overlay" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:contentDescription="@null" - android:scaleType="fitXY" /> - </FrameLayout> - - <!-- For sensors such as UDFPS, this view is used during custom measurement/layout to add extra - padding so that the biometric icon is always in the right physical position. --> - <Space android:id="@+id/space_below_icon" - android:layout_width="match_parent" - android:layout_height="12dp" /> - - <TextView - android:id="@+id/indicator" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingHorizontal="24dp" - android:textSize="12sp" - android:gravity="center_horizontal" - android:accessibilityLiveRegion="polite" - android:singleLine="true" - android:ellipsize="marquee" - android:marqueeRepeatLimit="marquee_forever" - android:scrollHorizontally="true" - android:fadingEdge="horizontal" - android:textColor="@color/biometric_dialog_gray"/> - - <LinearLayout - android:id="@+id/button_bar" - android:layout_width="match_parent" - android:layout_height="88dp" - style="?android:attr/buttonBarStyle" - android:orientation="horizontal" - android:paddingTop="24dp"> - - <Space android:id="@+id/leftSpacer" - android:layout_width="8dp" - android:layout_height="match_parent" - android:visibility="visible" /> - - <!-- Negative Button, reserved for app --> - <Button android:id="@+id/button_negative" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" - android:layout_gravity="center_vertical" - android:ellipsize="end" - android:maxLines="2" - android:maxWidth="@dimen/biometric_dialog_button_negative_max_width" - android:visibility="gone"/> - <!-- Cancel Button, replaces negative button when biometric is accepted --> - <Button android:id="@+id/button_cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" - android:layout_gravity="center_vertical" - android:maxWidth="@dimen/biometric_dialog_button_negative_max_width" - android:text="@string/cancel" - android:visibility="gone"/> - <!-- "Use Credential" Button, replaces if device credential is allowed --> - <Button android:id="@+id/button_use_credential" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" - android:layout_gravity="center_vertical" - android:maxWidth="@dimen/biometric_dialog_button_negative_max_width" - android:visibility="gone"/> - - <Space android:id="@+id/middleSpacer" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:visibility="visible"/> - - <!-- Positive Button --> - <Button android:id="@+id/button_confirm" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Colored" - android:layout_gravity="center_vertical" - android:ellipsize="end" - android:maxLines="2" - android:maxWidth="@dimen/biometric_dialog_button_positive_max_width" - android:text="@string/biometric_dialog_confirm" - android:visibility="gone"/> - <!-- Try Again Button --> - <Button android:id="@+id/button_try_again" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Colored" - android:layout_gravity="center_vertical" - android:ellipsize="end" - android:maxLines="2" - android:maxWidth="@dimen/biometric_dialog_button_positive_max_width" - android:text="@string/biometric_dialog_try_again" - android:visibility="gone"/> - - <Space android:id="@+id/rightSpacer" - android:layout_width="8dp" - android:layout_height="match_parent" - android:visibility="visible" /> - </LinearLayout> - -</com.android.systemui.biometrics.ui.BiometricPromptLayout> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index a5fd5b95315e..2ad6b6a4853c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -280,13 +280,17 @@ <!-- For updated Screen Recording permission dialog (i.e. with PSS)--> <!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]--> - <string name="screenrecord_permission_dialog_title">Start Recording?</string> + <string name="screenrecord_permission_dialog_title">Record your screen?</string> + <!-- Screen recording permission option for recording just a single app [CHAR LIMIT=50] --> + <string name="screenrecord_permission_dialog_option_text_single_app">Record one app</string> + <!-- Screen recording permission option for recording the whole screen [CHAR LIMIT=50] --> + <string name="screenrecord_permission_dialog_option_text_entire_screen">Record entire screen</string> <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> - <string name="screenrecord_permission_dialog_warning_entire_screen">While you’re recording, Android has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <string name="screenrecord_permission_dialog_warning_entire_screen">When you’re recording your entire screen, anything shown on your screen is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> - <string name="screenrecord_permission_dialog_warning_single_app">While you’re recording an app, Android has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> - <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]--> - <string name="screenrecord_permission_dialog_continue">Start recording</string> + <string name="screenrecord_permission_dialog_warning_single_app">When you’re recording an app, anything shown or played in that app is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <!-- Button to start a screen recording of the entire screen in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]--> + <string name="screenrecord_permission_dialog_continue_entire_screen">Record screen</string> <!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]--> <string name="screenrecord_audio_label">Record audio</string> @@ -1358,10 +1362,6 @@ <!-- Media projection permission dialog warning text for system services. [CHAR LIMIT=NONE] --> <string name="media_projection_sys_service_dialog_warning">The service providing this function will have access to all of the information that is visible on your screen or played from your device while recording or casting. This includes information such as passwords, payment details, photos, messages, and audio that you play.</string> - <!-- Permission dropdown option for sharing or recording the whole screen. [CHAR LIMIT=30] --> - <string name="screen_share_permission_dialog_option_entire_screen">Entire screen</string> - <!-- Permission dropdown option for sharing or recording single app. [CHAR LIMIT=30] --> - <string name="screen_share_permission_dialog_option_single_app">A single app</string> <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] --> <string name="screen_share_permission_app_selector_title">Share or record an app</string> diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt index 12d881b20ca1..c0b6acfb2acd 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics import android.Manifest +import android.app.ActivityTaskManager import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX @@ -35,6 +36,7 @@ import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.SensorPropertiesInternal import android.os.UserManager import android.util.DisplayMetrics +import android.util.Log import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager @@ -45,6 +47,8 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.biometrics.shared.model.PromptKind object Utils { + private const val TAG = "SysUIBiometricUtils" + /** Base set of layout flags for fingerprint overlay widgets. */ const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS = (WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or @@ -148,4 +152,39 @@ object Utils { draw(canvas) return bitmap } + + // LINT.IfChange + @JvmStatic + /** + * Checks if a client package is running in the background or it's a system app. + * + * @param clientPackage The name of the package to be checked. + * @param clientClassNameIfItIsConfirmDeviceCredentialActivity The class name of + * ConfirmDeviceCredentialActivity. + * @return Whether the client package is running in background + */ + fun ActivityTaskManager.isSystemAppOrInBackground( + context: Context, + clientPackage: String, + clientClassNameIfItIsConfirmDeviceCredentialActivity: String? + ): Boolean { + Log.v(TAG, "Checking if the authenticating is in background, clientPackage:$clientPackage") + val tasks = getTasks(Int.MAX_VALUE) + if (tasks == null || tasks.isEmpty()) { + Log.w(TAG, "No running tasks reported") + return false + } + + val topActivity = tasks[0].topActivity + val isSystemApp = isSystem(context, clientPackage) + val topPackageEqualsToClient = topActivity!!.packageName == clientPackage + val isClientConfirmDeviceCredentialActivity = + clientClassNameIfItIsConfirmDeviceCredentialActivity != null + // b/339532378: If it's ConfirmDeviceCredentialActivity, we need to check further on + // class name. + return !(isSystemApp || topPackageEqualsToClient) || + (isClientConfirmDeviceCredentialActivity && + topActivity.className != clientClassNameIfItIsConfirmDeviceCredentialActivity) + } + // LINT.ThenChange(frameworks/base/services/core/java/com/android/server/biometrics/Utils.java) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 9521be1f11a7..723587e60df1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -17,11 +17,9 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; -import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION; -import static com.android.systemui.Flags.constraintBp; import android.animation.Animator; import android.annotation.IntDef; @@ -30,8 +28,6 @@ import android.annotation.Nullable; import android.app.AlertDialog; import android.content.Context; import android.content.res.Configuration; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; @@ -41,17 +37,11 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.UserManager; import android.util.Log; -import android.view.Display; -import android.view.DisplayInfo; -import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -60,7 +50,6 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ScrollView; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; @@ -74,7 +63,6 @@ import com.android.systemui.biometrics.AuthController.ScaleFactorProvider; import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor; import com.android.systemui.biometrics.shared.model.BiometricModalities; import com.android.systemui.biometrics.shared.model.PromptKind; -import com.android.systemui.biometrics.ui.BiometricPromptLayout; import com.android.systemui.biometrics.ui.CredentialView; import com.android.systemui.biometrics.ui.binder.BiometricViewBinder; import com.android.systemui.biometrics.ui.binder.BiometricViewSizeBinder; @@ -111,7 +99,6 @@ public class AuthContainerView extends LinearLayout private static final int ANIMATION_DURATION_SHOW_MS = 250; private static final int ANIMATION_DURATION_AWAY_MS = 350; - private static final int ANIMATE_CREDENTIAL_START_DELAY_MS = 300; private static final int STATE_UNKNOWN = 0; private static final int STATE_ANIMATING_IN = 1; @@ -136,13 +123,11 @@ public class AuthContainerView extends LinearLayout private final Config mConfig; private final int mEffectiveUserId; - private final Handler mHandler; private final IBinder mWindowToken = new Binder(); private final WindowManager mWindowManager; private final Interpolator mLinearOutSlowIn; private final LockPatternUtils mLockPatternUtils; private final WakefulnessLifecycle mWakefulnessLifecycle; - private final AuthDialogPanelInteractionDetector mPanelInteractionDetector; private final InteractionJankMonitor mInteractionJankMonitor; private final CoroutineScope mApplicationCoroutineScope; @@ -159,10 +144,7 @@ public class AuthContainerView extends LinearLayout private final AuthPanelController mPanelController; private final ViewGroup mLayout; private final ImageView mBackgroundView; - private final ScrollView mBiometricScrollView; private final View mPanelView; - private final List<FingerprintSensorPropertiesInternal> mFpProps; - private final List<FaceSensorPropertiesInternal> mFaceProps; private final float mTranslationY; @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; private final Set<Integer> mFailedModalities = new HashSet<Integer>(); @@ -229,13 +211,7 @@ public class AuthContainerView extends LinearLayout @Override public void onUseDeviceCredential() { mConfig.mCallback.onDeviceCredentialPressed(getRequestId()); - if (constraintBp()) { - addCredentialView(false /* animatePanel */, true /* animateContents */); - } else { - mHandler.postDelayed(() -> { - addCredentialView(false /* animatePanel */, true /* animateContents */); - }, mConfig.mSkipAnimation ? 0 : ANIMATE_CREDENTIAL_START_DELAY_MS); - } + addCredentialView(false /* animatePanel */, true /* animateContents */); // TODO(b/313469218): Remove Config mConfig.mPromptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL); @@ -303,36 +279,12 @@ public class AuthContainerView extends LinearLayout @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, - @NonNull UserManager userManager, - @NonNull LockPatternUtils lockPatternUtils, - @NonNull InteractionJankMonitor jankMonitor, - @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor, - @NonNull PromptViewModel promptViewModel, - @NonNull Provider<CredentialViewModel> credentialViewModelProvider, - @NonNull @Background DelayableExecutor bgExecutor, - @NonNull VibratorHelper vibratorHelper) { - this(config, applicationCoroutineScope, fpProps, faceProps, - wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, - jankMonitor, promptSelectorInteractor, promptViewModel, - credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor, - vibratorHelper); - } - - @VisibleForTesting - AuthContainerView(@NonNull Config config, - @NonNull CoroutineScope applicationCoroutineScope, - @Nullable List<FingerprintSensorPropertiesInternal> fpProps, - @Nullable List<FaceSensorPropertiesInternal> faceProps, - @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider, @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, - @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper) { super(config.mContext); @@ -340,10 +292,8 @@ public class AuthContainerView extends LinearLayout mConfig = config; mLockPatternUtils = lockPatternUtils; mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId); - mHandler = mainHandler; mWindowManager = mContext.getSystemService(WindowManager.class); mWakefulnessLifecycle = wakefulnessLifecycle; - mPanelInteractionDetector = panelInteractionDetector; mApplicationCoroutineScope = applicationCoroutineScope; mPromptViewModel = promptViewModel; @@ -352,8 +302,6 @@ public class AuthContainerView extends LinearLayout mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mBiometricCallback = new BiometricCallback(); - mFpProps = fpProps; - mFaceProps = faceProps; final BiometricModalities biometricModalities = new BiometricModalities( Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds)); @@ -367,7 +315,7 @@ public class AuthContainerView extends LinearLayout final LayoutInflater layoutInflater = LayoutInflater.from(mContext); final PromptKind kind = mPromptViewModel.getPromptKind().getValue(); - if (constraintBp() && kind.isBiometric()) { + if (kind.isBiometric()) { if (kind.isTwoPaneLandscapeBiometric()) { mLayout = (ConstraintLayout) layoutInflater.inflate( R.layout.biometric_prompt_two_pane_layout, this, false /* attachToRoot */); @@ -379,26 +327,16 @@ public class AuthContainerView extends LinearLayout mLayout = (FrameLayout) layoutInflater.inflate( R.layout.auth_container_view, this, false /* attachToRoot */); } - mBiometricScrollView = mLayout.findViewById(R.id.biometric_scrollview); addView(mLayout); mBackgroundView = mLayout.findViewById(R.id.background); mPanelView = mLayout.findViewById(R.id.panel); - if (!constraintBp()) { - final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - android.R.attr.colorBackgroundFloating}); - mPanelView.setBackgroundColor(ta.getColor(0, Color.WHITE)); - ta.recycle(); - } mPanelController = new AuthPanelController(mContext, mPanelView); mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; mCredentialViewModelProvider = credentialViewModelProvider; - showPrompt(config, layoutInflater, promptViewModel, - Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), - Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds), - vibratorHelper); + showPrompt(promptViewModel, vibratorHelper); // TODO: De-dupe the logic with AuthCredentialPasswordView setOnKeyListener((v, keyCode, event) -> { @@ -415,52 +353,25 @@ public class AuthContainerView extends LinearLayout requestFocus(); } - private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater, - @NonNull PromptViewModel viewModel, - @Nullable FingerprintSensorPropertiesInternal fpProps, - @Nullable FaceSensorPropertiesInternal faceProps, - @NonNull VibratorHelper vibratorHelper - ) { + private void showPrompt(@NonNull PromptViewModel viewModel, + @NonNull VibratorHelper vibratorHelper) { if (mPromptViewModel.getPromptKind().getValue().isBiometric()) { - addBiometricView(config, layoutInflater, viewModel, fpProps, faceProps, vibratorHelper); + addBiometricView(viewModel, vibratorHelper); } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) { - if (constraintBp()) { - addCredentialView(true, false); - } + addCredentialView(true, false); } else { mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId()); } } - private void addBiometricView(@NonNull Config config, @NonNull LayoutInflater layoutInflater, - @NonNull PromptViewModel viewModel, - @Nullable FingerprintSensorPropertiesInternal fpProps, - @Nullable FaceSensorPropertiesInternal faceProps, + private void addBiometricView(@NonNull PromptViewModel viewModel, @NonNull VibratorHelper vibratorHelper) { - - if (constraintBp()) { - mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, null, - // TODO(b/201510778): This uses the wrong timeout in some cases - getJankListener(mLayout, TRANSIT, - BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), - mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, - vibratorHelper); - } else { - final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate( - R.layout.biometric_prompt_layout, null, false); - mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController, - // TODO(b/201510778): This uses the wrong timeout in some cases - getJankListener(view, TRANSIT, - BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), - mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, - vibratorHelper); - - // TODO(b/251476085): migrate these dependencies - if (fpProps != null && fpProps.isAnyUdfpsType()) { - view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps), - config.mScaleProvider); - } - } + mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, + // TODO(b/201510778): This uses the wrong timeout in some cases + getJankListener(mLayout, TRANSIT, + BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), + mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, + vibratorHelper); } @VisibleForTesting @@ -524,9 +435,6 @@ public class AuthContainerView extends LinearLayout @Override public void onOrientationChanged() { - if (!constraintBp()) { - updatePositionByCapability(true /* invalidate */); - } } @Override @@ -538,23 +446,6 @@ public class AuthContainerView extends LinearLayout } mWakefulnessLifecycle.addObserver(this); - if (constraintBp()) { - // Do nothing on attachment with constraintLayout - } else if (mPromptViewModel.getPromptKind().getValue().isBiometric()) { - mBiometricScrollView.addView(mBiometricView.asView()); - } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) { - addCredentialView(true /* animatePanel */, false /* animateContents */); - } else { - throw new IllegalStateException("Unknown configuration: " - + mConfig.mPromptInfo.getAuthenticators()); - } - - if (!constraintBp()) { - mPanelInteractionDetector.enable( - () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED)); - updatePositionByCapability(false /* invalidate */); - } - if (mConfig.mSkipIntro) { mContainerState = STATE_SHOWING; } else { @@ -618,120 +509,8 @@ public class AuthContainerView extends LinearLayout }; } - private void updatePositionByCapability(boolean forceInvalidate) { - final FingerprintSensorPropertiesInternal fpProp = Utils.findFirstSensorProperties( - mFpProps, mConfig.mSensorIds); - final FaceSensorPropertiesInternal faceProp = Utils.findFirstSensorProperties( - mFaceProps, mConfig.mSensorIds); - if (fpProp != null && fpProp.isAnyUdfpsType()) { - maybeUpdatePositionForUdfps(forceInvalidate /* invalidate */); - } - if (faceProp != null && mBiometricView != null && mBiometricView.isFaceOnly()) { - alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */); - } - if (fpProp != null && fpProp.sensorType == TYPE_POWER_BUTTON) { - alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */); - } - } - - private static boolean shouldUpdatePositionForUdfps(@NonNull View view) { - if (view instanceof BiometricPromptLayout) { - // this will force the prompt to align itself on the edge of the screen - // instead of centering (temporary workaround to prevent small implicit view - // from breaking due to the way gravity / margins are set in the legacy - // AuthPanelController - return true; - } - - return false; - } - - private boolean maybeUpdatePositionForUdfps(boolean invalidate) { - final Display display = getDisplay(); - if (display == null) { - return false; - } - - final DisplayInfo cachedDisplayInfo = new DisplayInfo(); - display.getDisplayInfo(cachedDisplayInfo); - if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) { - return false; - } - - final int displayRotation = cachedDisplayInfo.rotation; - switch (displayRotation) { - case Surface.ROTATION_0: - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - - case Surface.ROTATION_90: - mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); - setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); - break; - - case Surface.ROTATION_270: - mPanelController.setPosition(AuthPanelController.POSITION_LEFT); - setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); - break; - - case Surface.ROTATION_180: - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - } - - if (invalidate) { - mPanelView.invalidateOutline(); - } - - return true; - } - - private boolean alwaysUpdatePositionAtScreenBottom(boolean invalidate) { - final Display display = getDisplay(); - if (display == null) { - return false; - } - if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) { - return false; - } - - final int displayRotation = display.getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - case Surface.ROTATION_90: - case Surface.ROTATION_270: - case Surface.ROTATION_180: - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - } - - if (invalidate) { - mPanelView.invalidateOutline(); - } - - return true; - } - - private void setScrollViewGravity(int gravity) { - final FrameLayout.LayoutParams params = - (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams(); - params.gravity = gravity; - mBiometricScrollView.setLayoutParams(params); - } - @Override public void onDetachedFromWindow() { - mPanelInteractionDetector.disable(); OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); if (dispatcher != null) { findOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback); @@ -834,6 +613,11 @@ public class AuthContainerView extends LinearLayout } @Override + public String getClassNameIfItIsConfirmDeviceCredentialActivity() { + return mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity(); + } + + @Override public long getRequestId() { return mConfig.mRequestId; } @@ -878,7 +662,7 @@ public class AuthContainerView extends LinearLayout final Runnable endActionRunnable = () -> { setVisibility(View.INVISIBLE); - if (Flags.customBiometricPrompt() && constraintBp()) { + if (Flags.customBiometricPrompt()) { // TODO(b/288175645): resetPrompt calls should be lifecycle aware mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId()); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index b466f31cc509..037f5b72aff1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -22,7 +22,6 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.TaskStackListener; import android.content.BroadcastReceiver; @@ -173,7 +172,6 @@ public class AuthController implements @NonNull private final SparseBooleanArray mSfpsEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; - private final AuthDialogPanelInteractionDetector mPanelInteractionDetector; private boolean mAllFingerprintAuthenticatorsRegistered; @NonNull private final UserManager mUserManager; @NonNull private final LockPatternUtils mLockPatternUtils; @@ -187,7 +185,7 @@ public class AuthController implements final TaskStackListener mTaskStackListener = new TaskStackListener() { @Override public void onTaskStackChanged() { - if (!isOwnerInForeground()) { + if (isOwnerInBackground()) { mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground); } } @@ -227,21 +225,20 @@ public class AuthController implements } } - private boolean isOwnerInForeground() { + private boolean isOwnerInBackground() { if (mCurrentDialog != null) { final String clientPackage = mCurrentDialog.getOpPackageName(); - final List<ActivityManager.RunningTaskInfo> runningTasks = - mActivityTaskManager.getTasks(1); - if (!runningTasks.isEmpty()) { - final String topPackage = runningTasks.get(0).topActivity.getPackageName(); - if (!topPackage.contentEquals(clientPackage) - && !Utils.isSystem(mContext, clientPackage)) { - Log.w(TAG, "Evicting client due to: " + topPackage); - return false; - } + final String clientClassNameIfItIsConfirmDeviceCredentialActivity = + mCurrentDialog.getClassNameIfItIsConfirmDeviceCredentialActivity(); + final boolean isInBackground = Utils.isSystemAppOrInBackground(mActivityTaskManager, + mContext, clientPackage, + clientClassNameIfItIsConfirmDeviceCredentialActivity); + if (isInBackground) { + Log.w(TAG, "Evicting client due to top activity is not : " + clientPackage); } + return isInBackground; } - return true; + return false; } private void cancelIfOwnerIsNotInForeground() { @@ -728,7 +725,6 @@ public class AuthController implements Provider<UdfpsController> udfpsControllerFactory, @NonNull DisplayManager displayManager, @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull Lazy<UdfpsLogger> udfpsLogger, @@ -779,7 +775,6 @@ public class AuthController implements }); mWakefulnessLifecycle = wakefulnessLifecycle; - mPanelInteractionDetector = panelInteractionDetector; mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null; @@ -1229,7 +1224,6 @@ public class AuthController implements operationId, requestId, mWakefulnessLifecycle, - mPanelInteractionDetector, mUserManager, mLockPatternUtils, viewModel); @@ -1259,9 +1253,9 @@ public class AuthController implements } mCurrentDialog = newDialog; - // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be + // TODO(b/353597496): We should check whether |allowBackgroundAuthentication| should be // removed. - if (!promptInfo.isAllowBackgroundAuthentication() && !isOwnerInForeground()) { + if (!promptInfo.isAllowBackgroundAuthentication() && isOwnerInBackground()) { cancelIfOwnerIsNotInForeground(); } else { mCurrentDialog.show(mWindowManager); @@ -1306,7 +1300,6 @@ public class AuthController implements PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull PromptViewModel viewModel) { @@ -1323,7 +1316,7 @@ public class AuthController implements config.mSensorIds = sensorIds; config.mScaleProvider = this::getScaleFactor; return new AuthContainerView(config, mApplicationCoroutineScope, mFpProps, mFaceProps, - wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, + wakefulnessLifecycle, userManager, lockPatternUtils, mInteractionJankMonitor, mPromptSelectorInteractor, viewModel, mCredentialViewModelProvider, bgExecutor, mVibratorHelper); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index 3fd488c34121..861191671ba9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -94,6 +94,12 @@ public interface AuthDialog extends Dumpable { */ String getOpPackageName(); + /** + * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is + * not ConfirmDeviceCredentialActivity. + */ + String getClassNameIfItIsConfirmDeviceCredentialActivity(); + /** The requestId of the underlying operation within the framework. */ long getRequestId(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt deleted file mode 100644 index 04c2351c1a3e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt +++ /dev/null @@ -1,68 +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.annotation.MainThread -import android.util.Log -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import dagger.Lazy -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - -class AuthDialogPanelInteractionDetector -@Inject -constructor( - @Application private val scope: CoroutineScope, - private val shadeInteractorLazy: Lazy<ShadeInteractor>, -) { - private var shadeExpansionCollectorJob: Job? = null - - @MainThread - fun enable(onShadeInteraction: Runnable) { - if (shadeExpansionCollectorJob != null) { - Log.e(TAG, "Already enabled") - return - } - //TODO(b/313957306) delete this check - if (shadeInteractorLazy.get().isUserInteracting.value) { - // Workaround for b/311266890. This flow is in an error state that breaks this. - Log.e(TAG, "isUserInteracting already true, skipping enable") - return - } - shadeExpansionCollectorJob = - scope.launch { - Log.i(TAG, "Enable detector") - // wait for it to emit true once - shadeInteractorLazy.get().isUserInteracting.first { it } - Log.i(TAG, "Detector detected shade interaction") - onShadeInteraction.run() - } - shadeExpansionCollectorJob?.invokeOnCompletion { shadeExpansionCollectorJob = null } - } - - @MainThread - fun disable() { - Log.i(TAG, "Disable detector") - shadeExpansionCollectorJob?.cancel() - } -} - -private const val TAG = "AuthDialogPanelInteractionDetector" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java deleted file mode 100644 index 02eae9cedf74..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import android.annotation.IdRes; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.Insets; -import android.graphics.Rect; -import android.hardware.biometrics.SensorLocationInternal; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.os.Build; -import android.util.Log; -import android.view.Surface; -import android.view.View; -import android.view.View.MeasureSpec; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.WindowMetrics; -import android.widget.FrameLayout; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.res.R; - -/** - * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical - * under-display fingerprint sensor (UDFPS). - */ -public class UdfpsDialogMeasureAdapter { - private static final String TAG = "UdfpsDialogMeasurementAdapter"; - private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG; - - @NonNull private final ViewGroup mView; - @NonNull private final FingerprintSensorPropertiesInternal mSensorProps; - @Nullable private WindowManager mWindowManager; - private int mBottomSpacerHeight; - - public UdfpsDialogMeasureAdapter( - @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) { - mView = view; - mSensorProps = sensorProps; - mWindowManager = mView.getContext().getSystemService(WindowManager.class); - } - - @NonNull - FingerprintSensorPropertiesInternal getSensorProps() { - return mSensorProps; - } - - @NonNull - public AuthDialog.LayoutParams onMeasureInternal( - int width, int height, @NonNull AuthDialog.LayoutParams layoutParams, - float scaleFactor) { - - final int displayRotation = mView.getDisplay().getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - return onMeasureInternalPortrait(width, height, scaleFactor); - case Surface.ROTATION_90: - case Surface.ROTATION_270: - return onMeasureInternalLandscape(width, height, scaleFactor); - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - return layoutParams; - } - } - - /** - * @return the actual (and possibly negative) bottom spacer height. If negative, this indicates - * that the UDFPS sensor is too low. Our current xml and custom measurement logic is very hard - * too cleanly support this case. So, let's have the onLayout code translate the sensor location - * instead. - */ - public int getBottomSpacerHeight() { - return mBottomSpacerHeight; - } - - /** - * @return sensor diameter size as scaleFactor - */ - public int getSensorDiameter(float scaleFactor) { - return (int) (scaleFactor * mSensorProps.getLocation().sensorRadius * 2); - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height, - float scaleFactor) { - final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics(); - - // Figure out where the bottom of the sensor anim should be. - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - final int dialogMargin = getDialogMarginPx(); - final int displayHeight = getMaximumWindowBounds(windowMetrics).height(); - final Insets navbarInsets = getNavbarInsets(windowMetrics); - mBottomSpacerHeight = calculateBottomSpacerHeightForPortrait( - mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, - dialogMargin, navbarInsets.bottom, scaleFactor); - - // Go through each of the children and do the custom measurement. - int totalHeight = 0; - final int numChildren = mView.getChildCount(); - final int sensorDiameter = getSensorDiameter(scaleFactor); - for (int i = 0; i < numChildren; i++) { - final View child = mView.getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - } else if (child.getId() == R.id.space_above_icon - || child.getId() == R.id.space_above_content - || child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Set the spacer height so the fingerprint icon is on the physical sensor area - final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0); - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.description - || child.getId() == R.id.customized_view_container) { - //skip description view and compute later - continue; - } else if (child.getId() == R.id.logo) { - child.measure( - MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - totalHeight += child.getMeasuredHeight(); - } - } - - //re-calculate the height of body content - View description = mView.findViewById(R.id.description); - View contentView = mView.findViewById(R.id.customized_view_container); - if (description != null && description.getVisibility() != View.GONE) { - totalHeight += measureDescription(description, displayHeight, width, totalHeight); - } else if (contentView != null && contentView.getVisibility() != View.GONE) { - totalHeight += measureDescription(contentView, displayHeight, width, totalHeight); - } - - return new AuthDialog.LayoutParams(width, totalHeight); - } - - private int measureDescription(View bodyContent, int displayHeight, int currWidth, - int currHeight) { - int newHeight = bodyContent.getMeasuredHeight() + currHeight; - int limit = (int) (displayHeight * 0.75); - if (newHeight > limit) { - bodyContent.measure( - MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY)); - } - return bodyContent.getMeasuredHeight(); - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height, - float scaleFactor) { - final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics(); - - // Find the spacer height needed to vertically align the icon with the sensor. - final int titleHeight = getViewHeightPx(R.id.title); - final int subtitleHeight = getViewHeightPx(R.id.subtitle); - final int descriptionHeight = getViewHeightPx(R.id.description); - final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - - final Insets navbarInsets = getNavbarInsets(windowMetrics); - final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, - subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, - buttonBarHeight, navbarInsets.bottom); - - // Find the spacer width needed to horizontally align the icon with the sensor. - final int displayWidth = getMaximumWindowBounds(windowMetrics).width(); - final int dialogMargin = getDialogMarginPx(); - final int horizontalInset = navbarInsets.left + navbarInsets.right; - final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( - mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor); - - final int sensorDiameter = getSensorDiameter(scaleFactor); - final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; - - int remeasuredHeight = 0; - final int numChildren = mView.getChildCount(); - for (int i = 0; i < numChildren; i++) { - final View child = mView.getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - } else if (child.getId() == R.id.space_above_icon) { - // Adjust the width and height of the top spacer if necessary. - final int newTopSpacerHeight = child.getLayoutParams().height - - Math.min(bottomSpacerHeight, 0); - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.button_bar) { - // Adjust the width of the button bar while preserving its height. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Adjust the bottom spacer height to align the fingerprint icon with the sensor. - final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0); - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY)); - } else { - // Use the remeasured width for all other child views. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - remeasuredHeight += child.getMeasuredHeight(); - } - } - - return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); - } - - private int getViewHeightPx(@IdRes int viewId) { - final View view = mView.findViewById(viewId); - return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0; - } - - private int getDialogMarginPx() { - return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); - } - - @NonNull - private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) { - return windowMetrics != null - ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars()) - : Insets.NONE; - } - - @NonNull - private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) { - return windowMetrics != null ? windowMetrics.getBounds() : new Rect(); - } - - /** - * For devices in portrait orientation where the sensor is too high up, calculates the amount of - * padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForPortrait( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, - int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, - int navbarBottomInsetPx, float scaleFactor) { - final SensorLocationInternal location = sensorProperties.getLocation(); - final int sensorDistanceFromBottom = displayHeightPx - - (int) (scaleFactor * location.sensorLocationY) - - (int) (scaleFactor * location.sensorRadius); - - final int spacerHeight = sensorDistanceFromBottom - - textIndicatorHeightPx - - buttonBarHeightPx - - dialogMarginPx - - navbarBottomInsetPx; - - if (DEBUG) { - Log.d(TAG, "Display height: " + displayHeightPx - + ", Distance from bottom: " + sensorDistanceFromBottom - + ", Bottom margin: " + dialogMarginPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (portrait): " + spacerHeight - + ", Scale Factor: " + scaleFactor); - } - - return spacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too high up, calculates the amount - * of padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, - int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, - int buttonBarHeightPx, int navbarBottomInsetPx) { - - final int dialogHeightAboveIcon = titleHeightPx - + subtitleHeightPx - + descriptionHeightPx - + topSpacerHeightPx; - - final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; - - final int bottomSpacerHeight = dialogHeightAboveIcon - - dialogHeightBelowIcon - - navbarBottomInsetPx; - - if (DEBUG) { - Log.d(TAG, "Title height: " + titleHeightPx - + ", Subtitle height: " + subtitleHeightPx - + ", Description height: " + descriptionHeightPx - + ", Top spacer height: " + topSpacerHeightPx - + ", Text indicator height: " + textIndicatorHeightPx - + ", Button bar height: " + buttonBarHeightPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (landscape): " + bottomSpacerHeight); - } - - return bottomSpacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too left/right, calculates the - * amount of padding necessary to center the biometric icon within the sensor's physical - * location. - */ - @VisibleForTesting - static int calculateHorizontalSpacerWidthForLandscape( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, - int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) { - final SensorLocationInternal location = sensorProperties.getLocation(); - final int sensorDistanceFromEdge = displayWidthPx - - (int) (scaleFactor * location.sensorLocationY) - - (int) (scaleFactor * location.sensorRadius); - - final int horizontalPadding = sensorDistanceFromEdge - - dialogMarginPx - - navbarHorizontalInsetPx; - - if (DEBUG) { - Log.d(TAG, "Display width: " + displayWidthPx - + ", Distance from edge: " + sensorDistanceFromEdge - + ", Dialog margin: " + dialogMarginPx - + ", Navbar horizontal inset: " + navbarHorizontalInsetPx - + ", Horizontal spacer width (landscape): " + horizontalPadding - + ", Scale Factor: " + scaleFactor); - } - - return horizontalPadding; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 5e2b5ff5c1ac..6da5e42c12d9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -188,7 +188,6 @@ constructor( val hasCredentialViewShown = promptKind.value.isCredential() val showBpForCredential = Flags.customBiometricPrompt() && - com.android.systemui.Flags.constraintBp() && !Utils.isBiometricAllowed(promptInfo) && isDeviceCredentialAllowed(promptInfo) && promptInfo.contentView != null && diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index 348b4234a430..695707d782b7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -44,7 +44,7 @@ sealed class BiometricPromptRequest( val logoDescription: String? = info.logoDescription val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" val componentNameForConfirmDeviceCredentialActivity: ComponentName? = - info.componentNameForConfirmDeviceCredentialActivity + info.realCallerForConfirmDeviceCredentialActivity val allowBackgroundAuthentication = info.isAllowBackgroundAuthentication } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java deleted file mode 100644 index b450896729b7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ /dev/null @@ -1,188 +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; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.graphics.Insets; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.TextView; - -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; - -/** - * Contains the Biometric views (title, subtitle, icon, buttons, etc.). - * - * TODO(b/251476085): get the udfps junk out of here, at a minimum. Likely can be replaced with a - * normal LinearLayout. - */ -public class BiometricPromptLayout extends LinearLayout { - - private static final String TAG = "BiometricPromptLayout"; - - @NonNull - private final WindowManager mWindowManager; - @Nullable - private AuthController.ScaleFactorProvider mScaleFactorProvider; - @Nullable - private UdfpsDialogMeasureAdapter mUdfpsAdapter; - - private final boolean mUseCustomBpSize; - private final int mCustomBpWidth; - private final int mCustomBpHeight; - - public BiometricPromptLayout(Context context) { - this(context, null); - } - - public BiometricPromptLayout(Context context, AttributeSet attrs) { - super(context, attrs); - - mWindowManager = context.getSystemService(WindowManager.class); - - mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size); - mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width); - mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height); - } - - @Deprecated - public void setUdfpsAdapter(@NonNull UdfpsDialogMeasureAdapter adapter, - @NonNull AuthController.ScaleFactorProvider scaleProvider) { - mUdfpsAdapter = adapter; - mScaleFactorProvider = scaleProvider != null ? scaleProvider : () -> 1.0f; - } - - @Deprecated - public boolean isUdfps() { - return mUdfpsAdapter != null; - } - - @Deprecated - public Pair<Integer, Integer> getUpdatedFingerprintAffordanceSize() { - if (mUdfpsAdapter != null) { - final int sensorDiameter = mUdfpsAdapter.getSensorDiameter( - mScaleFactorProvider.provide()); - return new Pair(sensorDiameter, sensorDiameter); - } - return null; - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternal(int width, int height) { - int totalHeight = 0; - final int numChildren = getChildCount(); - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - - if (child.getId() == R.id.space_above_icon - || child.getId() == R.id.space_above_content - || child.getId() == R.id.space_below_icon - || child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.biometric_icon_frame) { - final View iconView = findViewById(R.id.biometric_icon); - child.measure( - MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.logo) { - child.measure( - MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.biometric_icon) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } else { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - totalHeight += child.getMeasuredHeight(); - } - } - - final AuthDialog.LayoutParams params = new AuthDialog.LayoutParams(width, totalHeight); - if (mUdfpsAdapter != null) { - return mUdfpsAdapter.onMeasureInternal(width, height, params, - (mScaleFactorProvider != null) ? mScaleFactorProvider.provide() : 1.0f); - } else { - return params; - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - - if (mUseCustomBpSize) { - width = mCustomBpWidth; - height = mCustomBpHeight; - } else { - width = Math.min(width, height); - } - - // add nav bar insets since the parent AuthContainerView - // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS - final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars()); - final AuthDialog.LayoutParams params = onMeasureInternal(width, height); - setMeasuredDimension(params.mMediumWidth + insets.left + insets.right, - params.mMediumHeight + insets.bottom); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mUdfpsAdapter != null) { - // Move the UDFPS icon and indicator text if necessary. This probably only needs to - // happen for devices where the UDFPS sensor is too low. - // TODO(b/201510778): Update this logic to support cases where the sensor or text - // overlap the button bar area. - final float bottomSpacerHeight = mUdfpsAdapter.getBottomSpacerHeight(); - Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight); - if (bottomSpacerHeight < 0) { - final FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame); - iconFrame.setTranslationY(-bottomSpacerHeight); - final TextView indicator = findViewById(R.id.indicator); - indicator.setTranslationY(-bottomSpacerHeight); - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index 7ccac03bcac6..0b474f8092a4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -38,14 +38,13 @@ import android.widget.LinearLayout import android.widget.Space import android.widget.TextView import com.android.settingslib.Utils -import com.android.systemui.biometrics.ui.BiometricPromptLayout import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlin.math.ceil private const val TAG = "BiometricCustomizedViewBinder" -/** Sub-binder for [BiometricPromptLayout.customized_view_container]. */ +/** Sub-binder for Biometric Prompt Customized View */ object BiometricCustomizedViewBinder { fun bind( customizedViewContainer: LinearLayout, 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 43ba097684d6..a20a17f13a42 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 @@ -45,13 +45,10 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory -import com.android.systemui.Flags.constraintBp -import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.biometrics.shared.model.asBiometricModality -import com.android.systemui.biometrics.ui.BiometricPromptLayout import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode import com.android.systemui.biometrics.ui.viewmodel.PromptMessage import com.android.systemui.biometrics.ui.viewmodel.PromptSize @@ -72,28 +69,18 @@ private const val MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30 /** Top-most view binder for BiometricPrompt views. */ object BiometricViewBinder { - /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ + /** Binds a Biometric Prompt View to a [PromptViewModel]. */ @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( view: View, viewModel: PromptViewModel, - panelViewController: AuthPanelController?, jankListener: BiometricJankListener, backgroundView: View, legacyCallback: Spaghetti.Callback, applicationScope: CoroutineScope, vibratorHelper: VibratorHelper, ): Spaghetti { - /** - * View is only set visible in BiometricViewSizeBinder once PromptSize is determined that - * accounts for iconView size, to prevent prompt resizing being visible to the user. - * - * TODO(b/288175072): May be able to remove this once constraint layout is implemented - */ - if (!constraintBp()) { - view.visibility = View.INVISIBLE - } val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! val textColorError = @@ -104,9 +91,7 @@ object BiometricViewBinder { R.style.TextAppearance_AuthCredential_Indicator, intArrayOf(android.R.attr.textColor) ) - val textColorHint = - if (constraintBp()) attributes.getColor(0, 0) - else view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + val textColorHint = attributes.getColor(0, 0) attributes.recycle() val logoView = view.requireViewById<ImageView>(R.id.logo) @@ -116,12 +101,7 @@ object BiometricViewBinder { val descriptionView = view.requireViewById<TextView>(R.id.description) val customizedViewContainer = view.requireViewById<LinearLayout>(R.id.customized_view_container) - val udfpsGuidanceView = - if (constraintBp()) { - view.requireViewById<View>(R.id.panel) - } else { - backgroundView - } + val udfpsGuidanceView = view.requireViewById<View>(R.id.panel) // set selected to enable marquee unless a screen reader is enabled titleView.isSelected = @@ -130,14 +110,6 @@ object BiometricViewBinder { !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) - - val iconSizeOverride = - if (constraintBp()) { - null - } else { - (view as BiometricPromptLayout).updatedFingerprintAffordanceSize - } - val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) // Negative-side (left) buttons @@ -213,7 +185,7 @@ object BiometricViewBinder { subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() - if (Flags.customBiometricPrompt() && constraintBp()) { + if (Flags.customBiometricPrompt()) { BiometricCustomizedViewBinder.bind( customizedViewContainer, viewModel.contentView.first(), @@ -250,22 +222,6 @@ object BiometricViewBinder { descriptionView, customizedViewContainer, ), - viewsToFadeInOnSizeChange = - listOf( - logoView, - logoDescriptionView, - titleView, - subtitleView, - descriptionView, - customizedViewContainer, - indicatorMessageView, - negativeButton, - cancelButton, - retryButton, - confirmationButton, - credentialFallbackButton, - ), - panelViewController = panelViewController, jankListener = jankListener, ) } @@ -275,7 +231,6 @@ object BiometricViewBinder { if (!showWithoutIcon) { PromptIconViewBinder.bind( iconView, - iconSizeOverride, viewModel, ) } @@ -329,20 +284,6 @@ object BiometricViewBinder { } } - // set padding - launch { - viewModel.promptPadding.collect { promptPadding -> - if (!constraintBp()) { - view.setPadding( - promptPadding.left, - promptPadding.top, - promptPadding.right, - promptPadding.bottom - ) - } - } - } - // configure & hide/disable buttons launch { viewModel.credentialKind @@ -546,24 +487,6 @@ class Spaghetti( fun onAuthenticatedAndConfirmed() } - @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") - enum class BiometricState { - /** Authentication hardware idle. */ - STATE_IDLE, - /** UI animating in, authentication hardware active. */ - STATE_AUTHENTICATING_ANIMATING_IN, - /** UI animated in, authentication hardware active. */ - STATE_AUTHENTICATING, - /** UI animated in, authentication hardware active. */ - STATE_HELP, - /** Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle. */ - STATE_ERROR, - /** Authenticated, waiting for user confirmation. Authentication hardware idle. */ - STATE_PENDING_CONFIRMATION, - /** Authenticated, dialog animating away soon. */ - STATE_AUTHENTICATED, - } - private var lifecycleScope: CoroutineScope? = null private var modalities: BiometricModalities = BiometricModalities() private var legacyCallback: Callback? = null @@ -699,15 +622,8 @@ class Spaghetti( } fun startTransitionToCredentialUI(isError: Boolean) { - if (!constraintBp()) { - applicationScope.launch { - viewModel.onSwitchToCredential() - legacyCallback?.onUseDeviceCredential() - } - } else { - viewModel.onSwitchToCredential() - legacyCallback?.onUseDeviceCredential() - } + viewModel.onSwitchToCredential() + legacyCallback?.onUseDeviceCredential() } fun cancelAnimation() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index b9ec2de63269..85c3ae3f214e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -38,10 +38,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.Guideline import androidx.core.animation.addListener import androidx.core.view.doOnLayout -import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope -import com.android.systemui.Flags.constraintBp -import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.ui.viewmodel.PromptPosition import com.android.systemui.biometrics.ui.viewmodel.PromptSize @@ -49,7 +46,6 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.biometrics.ui.viewmodel.isLarge import com.android.systemui.biometrics.ui.viewmodel.isLeft import com.android.systemui.biometrics.ui.viewmodel.isMedium -import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall import com.android.systemui.biometrics.ui.viewmodel.isSmall import com.android.systemui.biometrics.ui.viewmodel.isTop import com.android.systemui.lifecycle.repeatWhenAttached @@ -71,8 +67,6 @@ object BiometricViewSizeBinder { view: View, viewModel: PromptViewModel, viewsToHideWhenSmall: List<View>, - viewsToFadeInOnSizeChange: List<View>, - panelViewController: AuthPanelController?, jankListener: BiometricJankListener, ) { val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java)) @@ -92,553 +86,366 @@ object BiometricViewSizeBinder { } } - if (constraintBp()) { - val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline) - val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline) - val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline) - val midGuideline = view.findViewById<Guideline>(R.id.midGuideline) - - val iconHolderView = view.requireViewById<View>(R.id.biometric_icon) - val panelView = view.requireViewById<View>(R.id.panel) - val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size) - val pxToDp = - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 1f, - view.resources.displayMetrics - ) - val cornerRadiusPx = (pxToDp * cornerRadius).toInt() - - var currentSize: PromptSize? = null - var currentPosition: PromptPosition = PromptPosition.Bottom - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - when (currentPosition) { - PromptPosition.Right -> { - outline.setRoundRect( - 0, - 0, - view.width + cornerRadiusPx, - view.height, - cornerRadiusPx.toFloat() - ) - } - PromptPosition.Left -> { - outline.setRoundRect( - -cornerRadiusPx, - 0, - view.width, - view.height, - cornerRadiusPx.toFloat() - ) - } - PromptPosition.Bottom, - PromptPosition.Top -> { - outline.setRoundRect( - 0, - 0, - view.width, - view.height + cornerRadiusPx, - cornerRadiusPx.toFloat() - ) - } + val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline) + val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline) + val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline) + val midGuideline = view.findViewById<Guideline>(R.id.midGuideline) + + val iconHolderView = view.requireViewById<View>(R.id.biometric_icon) + val panelView = view.requireViewById<View>(R.id.panel) + val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size) + val pxToDp = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 1f, + view.resources.displayMetrics + ) + val cornerRadiusPx = (pxToDp * cornerRadius).toInt() + + var currentSize: PromptSize? = null + var currentPosition: PromptPosition = PromptPosition.Bottom + panelView.outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + when (currentPosition) { + PromptPosition.Right -> { + outline.setRoundRect( + 0, + 0, + view.width + cornerRadiusPx, + view.height, + cornerRadiusPx.toFloat() + ) } - } - } - - // ConstraintSets for animating between prompt sizes - val mediumConstraintSet = ConstraintSet() - mediumConstraintSet.clone(view as ConstraintLayout) - - val smallConstraintSet = ConstraintSet() - smallConstraintSet.clone(mediumConstraintSet) - - val largeConstraintSet = ConstraintSet() - largeConstraintSet.clone(mediumConstraintSet) - largeConstraintSet.constrainMaxWidth(R.id.panel, 0) - largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0) - largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0) - - // TODO: Investigate better way to handle 180 rotations - val flipConstraintSet = ConstraintSet() - - view.doOnLayout { - fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) { - viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } - largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) - largeConstraintSet.setVisibility(R.id.indicator, View.GONE) - largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) - - if (hideSensorIcon) { - smallConstraintSet.setVisibility(iconHolderView.id, View.GONE) - smallConstraintSet.setVisibility(R.id.indicator, View.GONE) - mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE) - mediumConstraintSet.setVisibility(R.id.indicator, View.GONE) - } - } - - view.repeatWhenAttached { - lifecycleScope.launch { - viewModel.iconPosition.collect { position -> - if (position != Rect()) { - val iconParams = - iconHolderView.layoutParams as ConstraintLayout.LayoutParams - - if (position.left != 0) { - iconParams.endToEnd = ConstraintSet.UNSET - iconParams.leftMargin = position.left - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.RIGHT - ) - mediumConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.LEFT, - ConstraintSet.PARENT_ID, - ConstraintSet.LEFT - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.LEFT, - position.left - ) - smallConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.RIGHT - ) - smallConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.LEFT, - ConstraintSet.PARENT_ID, - ConstraintSet.LEFT - ) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.LEFT, - position.left - ) - } - if (position.top != 0) { - iconParams.bottomToBottom = ConstraintSet.UNSET - iconParams.topMargin = position.top - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.BOTTOM - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.TOP, - position.top - ) - smallConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.BOTTOM - ) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.TOP, - position.top - ) - } - if (position.right != 0) { - iconParams.startToStart = ConstraintSet.UNSET - iconParams.rightMargin = position.right - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.LEFT - ) - mediumConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.RIGHT, - ConstraintSet.PARENT_ID, - ConstraintSet.RIGHT - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.RIGHT, - position.right - ) - smallConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.LEFT - ) - smallConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.RIGHT, - ConstraintSet.PARENT_ID, - ConstraintSet.RIGHT - ) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.RIGHT, - position.right - ) - } - if (position.bottom != 0) { - iconParams.topToTop = ConstraintSet.UNSET - iconParams.bottomMargin = position.bottom - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.TOP - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.BOTTOM, - position.bottom - ) - smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.BOTTOM, - position.bottom - ) - } - iconHolderView.layoutParams = iconParams - } + PromptPosition.Left -> { + outline.setRoundRect( + -cornerRadiusPx, + 0, + view.width, + view.height, + cornerRadiusPx.toFloat() + ) } - } - - lifecycleScope.launch { - viewModel.iconSize.collect { iconSize -> - iconHolderView.layoutParams.width = iconSize.first - iconHolderView.layoutParams.height = iconSize.second - mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first) - mediumConstraintSet.constrainHeight( - R.id.biometric_icon, - iconSize.second + PromptPosition.Bottom, + PromptPosition.Top -> { + outline.setRoundRect( + 0, + 0, + view.width, + view.height + cornerRadiusPx, + cornerRadiusPx.toFloat() ) } } + } + } - lifecycleScope.launch { - viewModel.guidelineBounds.collect { bounds -> - val bottomInset = - windowManager.maximumWindowMetrics.windowInsets - .getInsets(WindowInsets.Type.navigationBars()) - .bottom - mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset) - - if (bounds.left >= 0) { - mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) - smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) - } else if (bounds.left < 0) { - mediumConstraintSet.setGuidelineEnd( - leftGuideline.id, - abs(bounds.left) + // ConstraintSets for animating between prompt sizes + val mediumConstraintSet = ConstraintSet() + mediumConstraintSet.clone(view as ConstraintLayout) + + val smallConstraintSet = ConstraintSet() + smallConstraintSet.clone(mediumConstraintSet) + + val largeConstraintSet = ConstraintSet() + largeConstraintSet.clone(mediumConstraintSet) + largeConstraintSet.constrainMaxWidth(R.id.panel, 0) + largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0) + largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0) + + // TODO: Investigate better way to handle 180 rotations + val flipConstraintSet = ConstraintSet() + + view.doOnLayout { + fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) { + viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } + largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) + largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + largeConstraintSet.setVisibility(R.id.indicator, View.GONE) + largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) + + if (hideSensorIcon) { + smallConstraintSet.setVisibility(iconHolderView.id, View.GONE) + smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + smallConstraintSet.setVisibility(R.id.indicator, View.GONE) + mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE) + mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + mediumConstraintSet.setVisibility(R.id.indicator, View.GONE) + } + } + + view.repeatWhenAttached { + lifecycleScope.launch { + viewModel.iconPosition.collect { position -> + if (position != Rect()) { + val iconParams = + iconHolderView.layoutParams as ConstraintLayout.LayoutParams + + if (position.left != 0) { + iconParams.endToEnd = ConstraintSet.UNSET + iconParams.leftMargin = position.left + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT) + mediumConstraintSet.connect( + R.id.biometric_icon, + ConstraintSet.LEFT, + ConstraintSet.PARENT_ID, + ConstraintSet.LEFT ) - smallConstraintSet.setGuidelineEnd( - leftGuideline.id, - abs(bounds.left) + mediumConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.LEFT, + position.left ) - } - - if (bounds.right >= 0) { - mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) - smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) - } else if (bounds.right < 0) { - mediumConstraintSet.setGuidelineBegin( - rightGuideline.id, - abs(bounds.right) + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT) + smallConstraintSet.connect( + R.id.biometric_icon, + ConstraintSet.LEFT, + ConstraintSet.PARENT_ID, + ConstraintSet.LEFT ) - smallConstraintSet.setGuidelineBegin( - rightGuideline.id, - abs(bounds.right) + smallConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.LEFT, + position.left ) } - - if (bounds.top >= 0) { - mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) - smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) - } else if (bounds.top < 0) { - mediumConstraintSet.setGuidelineEnd( - topGuideline.id, - abs(bounds.top) + if (position.top != 0) { + iconParams.bottomToBottom = ConstraintSet.UNSET + iconParams.topMargin = position.top + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM) + mediumConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.TOP, + position.top ) - smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) - } - - if (midGuideline != null) { - val left = - if (bounds.left >= 0) { - abs(bounds.left) - } else { - view.width - abs(bounds.left) - } - val right = - if (bounds.right >= 0) { - view.width - abs(bounds.right) - } else { - abs(bounds.right) - } - val mid = (left + right) / 2 - mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid) - } - } - } - - lifecycleScope.launch { - combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect { - (hideSensorIcon, size) -> - setVisibilities(hideSensorIcon, size) - } - } - - lifecycleScope.launch { - combine(viewModel.position, viewModel.size, ::Pair).collect { - (position, size) -> - if (position.isLeft) { - if (size.isSmall) { - flipConstraintSet.clone(smallConstraintSet) - } else { - flipConstraintSet.clone(mediumConstraintSet) - } - - // Move all content to other panel - flipConstraintSet.connect( - R.id.scrollView, - ConstraintSet.LEFT, - R.id.midGuideline, - ConstraintSet.LEFT + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM) + smallConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.TOP, + position.top ) - flipConstraintSet.connect( - R.id.scrollView, + } + if (position.right != 0) { + iconParams.startToStart = ConstraintSet.UNSET + iconParams.rightMargin = position.right + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT) + mediumConstraintSet.connect( + R.id.biometric_icon, ConstraintSet.RIGHT, - R.id.rightGuideline, + ConstraintSet.PARENT_ID, ConstraintSet.RIGHT ) - } else if (position.isTop) { - // Top position is only used for 180 rotation Udfps - // Requires repositioning due to sensor location at top of screen - mediumConstraintSet.connect( - R.id.scrollView, - ConstraintSet.TOP, - R.id.indicator, - ConstraintSet.BOTTOM + mediumConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.RIGHT, + position.right ) - mediumConstraintSet.connect( - R.id.scrollView, - ConstraintSet.BOTTOM, - R.id.button_bar, - ConstraintSet.TOP + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT) + smallConstraintSet.connect( + R.id.biometric_icon, + ConstraintSet.RIGHT, + ConstraintSet.PARENT_ID, + ConstraintSet.RIGHT ) - mediumConstraintSet.connect( - R.id.panel, - ConstraintSet.TOP, + smallConstraintSet.setMargin( R.id.biometric_icon, - ConstraintSet.TOP + ConstraintSet.RIGHT, + position.right ) + } + if (position.bottom != 0) { + iconParams.topToTop = ConstraintSet.UNSET + iconParams.bottomMargin = position.bottom + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP) mediumConstraintSet.setMargin( - R.id.panel, - ConstraintSet.TOP, - (-24 * pxToDp).toInt() + R.id.biometric_icon, + ConstraintSet.BOTTOM, + position.bottom + ) + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP) + smallConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.BOTTOM, + position.bottom ) - mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f) } + iconHolderView.layoutParams = iconParams + } + } + } - when { - size.isSmall -> { - if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else { - smallConstraintSet.applyTo(view) - } - } - size.isMedium && currentSize.isSmall -> { - val autoTransition = AutoTransition() - autoTransition.setDuration( - ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong() - ) - - TransitionManager.beginDelayedTransition(view, autoTransition) - - if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else { - mediumConstraintSet.applyTo(view) - } - } - size.isMedium -> { - if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else { - mediumConstraintSet.applyTo(view) - } - } - size.isLarge && currentSize.isMedium -> { - val autoTransition = AutoTransition() - autoTransition.setDuration( - ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() - ) - - TransitionManager.beginDelayedTransition(view, autoTransition) - largeConstraintSet.applyTo(view) - } - } + lifecycleScope.launch { + viewModel.iconSize.collect { iconSize -> + iconHolderView.layoutParams.width = iconSize.first + iconHolderView.layoutParams.height = iconSize.second + mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first) + mediumConstraintSet.constrainHeight(R.id.biometric_icon, iconSize.second) + } + } + + lifecycleScope.launch { + viewModel.guidelineBounds.collect { bounds -> + val bottomInset = + windowManager.maximumWindowMetrics.windowInsets + .getInsets(WindowInsets.Type.navigationBars()) + .bottom + mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset) + + if (bounds.left >= 0) { + mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) + smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) + } else if (bounds.left < 0) { + mediumConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left)) + smallConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left)) + } + + if (bounds.right >= 0) { + mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) + smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) + } else if (bounds.right < 0) { + mediumConstraintSet.setGuidelineBegin( + rightGuideline.id, + abs(bounds.right) + ) + smallConstraintSet.setGuidelineBegin( + rightGuideline.id, + abs(bounds.right) + ) + } - currentSize = size - currentPosition = position - notifyAccessibilityChanged() + if (bounds.top >= 0) { + mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) + smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) + } else if (bounds.top < 0) { + mediumConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) + smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) + } - panelView.invalidateOutline() - view.invalidate() - view.requestLayout() + if (midGuideline != null) { + val left = + if (bounds.left >= 0) { + abs(bounds.left) + } else { + view.width - abs(bounds.left) + } + val right = + if (bounds.right >= 0) { + view.width - abs(bounds.right) + } else { + abs(bounds.right) + } + val mid = (left + right) / 2 + mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid) } } } - } - } else if (panelViewController != null) { - val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame) - val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding) - val fullSizeYOffset = - view.resources.getDimension( - R.dimen.biometric_dialog_medium_to_large_translation_offset - ) - - // cache the original position of the icon view (as done in legacy view) - // this must happen before any size changes can be made - view.doOnLayout { - // TODO(b/251476085): this old way of positioning has proven itself unreliable - // remove this and associated thing like (UdfpsDialogMeasureAdapter) and - // pin to the physical sensor - val iconHolderOriginalY = iconHolderView.y - - // bind to prompt - // TODO(b/251476085): migrate the legacy panel controller and simplify this - view.repeatWhenAttached { - var currentSize: PromptSize? = null - lifecycleScope.launch { - /** - * View is only set visible in BiometricViewSizeBinder once PromptSize is - * determined that accounts for iconView size, to prevent prompt resizing - * being visible to the user. - * - * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint - * layout is implemented - */ - combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect { - (isIconViewLoaded, size) -> - if (!isIconViewLoaded) { - return@collect - } - // prepare for animated size transitions - for (v in viewsToHideWhenSmall) { - v.showContentOrHide(forceHide = size.isSmall) - } + lifecycleScope.launch { + combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect { + (hideSensorIcon, size) -> + setVisibilities(hideSensorIcon, size) + } + } - if (viewModel.hideSensorIcon.first()) { - iconHolderView.visibility = View.GONE + lifecycleScope.launch { + combine(viewModel.position, viewModel.size, ::Pair).collect { (position, size) + -> + if (position.isLeft) { + if (size.isSmall) { + flipConstraintSet.clone(smallConstraintSet) + } else { + flipConstraintSet.clone(mediumConstraintSet) } - if (currentSize == null && size.isSmall) { - iconHolderView.alpha = 0f - } - if ((currentSize.isSmall && size.isMedium) || size.isSmall) { - viewsToFadeInOnSizeChange.forEach { it.alpha = 0f } + // Move all content to other panel + flipConstraintSet.connect( + R.id.scrollView, + ConstraintSet.LEFT, + R.id.midGuideline, + ConstraintSet.LEFT + ) + flipConstraintSet.connect( + R.id.scrollView, + ConstraintSet.RIGHT, + R.id.rightGuideline, + ConstraintSet.RIGHT + ) + } else if (position.isTop) { + // Top position is only used for 180 rotation Udfps + // Requires repositioning due to sensor location at top of screen + mediumConstraintSet.connect( + R.id.scrollView, + ConstraintSet.TOP, + R.id.indicator, + ConstraintSet.BOTTOM + ) + mediumConstraintSet.connect( + R.id.scrollView, + ConstraintSet.BOTTOM, + R.id.button_bar, + ConstraintSet.TOP + ) + mediumConstraintSet.connect( + R.id.panel, + ConstraintSet.TOP, + R.id.biometric_icon, + ConstraintSet.TOP + ) + mediumConstraintSet.setMargin( + R.id.panel, + ConstraintSet.TOP, + (-24 * pxToDp).toInt() + ) + mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f) + } + + when { + size.isSmall -> { + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + smallConstraintSet.applyTo(view) + } } + size.isMedium && currentSize.isSmall -> { + val autoTransition = AutoTransition() + autoTransition.setDuration( + ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong() + ) - // propagate size changes to legacy panel controller and animate - // transitions - view.doOnLayout { - val width = view.measuredWidth - val height = view.measuredHeight - - when { - size.isSmall -> { - iconHolderView.alpha = 1f - val bottomInset = - windowManager.maximumWindowMetrics.windowInsets - .getInsets(WindowInsets.Type.navigationBars()) - .bottom - iconHolderView.y = - if (view.isLandscape()) { - (view.height - - iconHolderView.height - - bottomInset) / 2f - } else { - view.height - - iconHolderView.height - - iconPadding - - bottomInset - } - val newHeight = - iconHolderView.height + (2 * iconPadding.toInt()) - - iconHolderView.paddingTop - - iconHolderView.paddingBottom - panelViewController.updateForContentDimensions( - width, - newHeight + bottomInset, - 0, /* animateDurationMs */ - ) - } - size.isMedium && currentSize.isSmall -> { - val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS - panelViewController.updateForContentDimensions( - width, - height, - duration, - ) - startMonitoredAnimation( - listOf( - iconHolderView.asVerticalAnimator( - duration = duration.toLong(), - toY = - iconHolderOriginalY - - viewsToHideWhenSmall - .filter { it.isGone } - .sumOf { it.height }, - ), - viewsToFadeInOnSizeChange.asFadeInAnimator( - duration = duration.toLong(), - delay = duration.toLong(), - ), - ) - ) - } - size.isMedium && currentSize.isNullOrNotSmall -> { - panelViewController.updateForContentDimensions( - width, - height, - 0, /* animateDurationMs */ - ) - } - size.isLarge -> { - val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS - panelViewController.setUseFullScreen(true) - panelViewController.updateForContentDimensions( - panelViewController.containerWidth, - panelViewController.containerHeight, - duration, - ) - - startMonitoredAnimation( - listOf( - view.asVerticalAnimator( - duration.toLong() * 2 / 3, - toY = view.y - fullSizeYOffset - ), - listOf(view) - .asFadeInAnimator( - duration = duration.toLong() / 2, - delay = duration.toLong(), - ), - ) - ) - // TODO(b/251476085): clean up (copied from legacy) - if (view.isAttachedToWindow) { - val parent = view.parent as? ViewGroup - parent?.removeView(view) - } - } + TransitionManager.beginDelayedTransition(view, autoTransition) + + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + mediumConstraintSet.applyTo(view) + } + } + size.isMedium -> { + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + mediumConstraintSet.applyTo(view) } + } + size.isLarge && currentSize.isMedium -> { + val autoTransition = AutoTransition() + autoTransition.setDuration( + ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() + ) - currentSize = size - view.visibility = View.VISIBLE - viewModel.setIsIconViewLoaded(false) - notifyAccessibilityChanged() + TransitionManager.beginDelayedTransition(view, autoTransition) + largeConstraintSet.applyTo(view) } } + + currentSize = size + currentPosition = position + notifyAccessibilityChanged() + + panelView.invalidateOutline() + view.invalidate() + view.requestLayout() } } } @@ -646,17 +453,6 @@ object BiometricViewSizeBinder { } } -private fun View.isLandscape(): Boolean { - val r = context.display.rotation - return if ( - context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) - ) { - r == Surface.ROTATION_0 || r == Surface.ROTATION_180 - } else { - r == Surface.ROTATION_90 || r == Surface.ROTATION_270 - } -} - private fun View.showContentOrHide(forceHide: Boolean = false) { val isTextViewWithBlankText = this is TextView && this.text.isBlank() val isImageViewWithoutImage = this is ImageView && this.drawable == null @@ -667,26 +463,3 @@ private fun View.showContentOrHide(forceHide: Boolean = false) { View.VISIBLE } } - -private fun View.asVerticalAnimator( - duration: Long, - toY: Float, - fromY: Float = this.y -): ValueAnimator { - val animator = ValueAnimator.ofFloat(fromY, toY) - animator.duration = duration - animator.addUpdateListener { y = it.animatedValue as Float } - return animator -} - -private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator { - forEach { it.alpha = 0f } - val animator = ValueAnimator.ofFloat(0f, 1f) - animator.duration = duration - animator.startDelay = delay - animator.addUpdateListener { - val alpha = it.animatedValue as Float - forEach { view -> view.alpha = alpha } - } - return animator -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index 18e2a56e5e78..49f4b05e2cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -10,7 +10,6 @@ import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.CredentialPasswordView import com.android.systemui.biometrics.ui.CredentialPatternView @@ -82,7 +81,7 @@ object CredentialViewBinder { subtitleView.textOrHide = header.subtitle descriptionView.textOrHide = header.description - if (Flags.customBiometricPrompt() && constraintBp()) { + if (Flags.customBiometricPrompt()) { BiometricCustomizedViewBinder.bind( customizedViewContainer, header.contentView, 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 index 9e4aaaa44085..eab3b26e9b68 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -22,9 +22,7 @@ import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieOnCompositionLoadedListener import com.android.settingslib.widget.LottieColorUtils -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel @@ -44,55 +42,12 @@ object PromptIconViewBinder { @JvmStatic fun bind( iconView: LottieAnimationView, - iconViewLayoutParamSizeOverride: Pair<Int, Int>?, promptViewModel: PromptViewModel ) { val viewModel = promptViewModel.iconViewModel 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 - } - - if (!constraintBp()) { - launch { - var lottieOnCompositionLoadedListener: LottieOnCompositionLoadedListener? = - null - - viewModel.iconSize.collect { iconSize -> - /** - * When we bind the BiometricPrompt View and ViewModel in - * [BiometricViewBinder], the view is set invisible and - * [isIconViewLoaded] is set to false. We configure the iconView with a - * LottieOnCompositionLoadedListener that sets [isIconViewLoaded] to - * true, in order to wait for the iconView to load before determining - * the prompt size, and prevent any prompt resizing from being visible - * to the user. - * - * TODO(b/288175072): May be able to remove this once constraint layout - * is unflagged - */ - if (lottieOnCompositionLoadedListener != null) { - iconView.removeLottieOnCompositionLoadedListener( - lottieOnCompositionLoadedListener!! - ) - } - lottieOnCompositionLoadedListener = LottieOnCompositionLoadedListener { - promptViewModel.setIsIconViewLoaded(true) - } - iconView.addLottieOnCompositionLoadedListener( - lottieOnCompositionLoadedListener!! - ) - - if (iconViewLayoutParamSizeOverride == null) { - iconView.layoutParams.width = iconSize.first - iconView.layoutParams.height = iconSize.second - } - } - } - } launch { viewModel.iconAsset @@ -154,7 +109,7 @@ fun LottieAnimationView.updateAsset( setAnimation(asset) if (animatingFromSfpsAuthenticating(asset)) { // Skipping to error / success / unlock segment of animation - setMinFrame(151) + setMinFrame(158) } else { frame = 0 } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt index 31af126eb3f0..761c3da77a4d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -6,7 +6,6 @@ import android.hardware.biometrics.Flags.customBiometricPrompt import android.hardware.biometrics.PromptContentView import android.text.InputType import com.android.internal.widget.LockPatternView -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.CredentialStatus import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor @@ -39,7 +38,7 @@ constructor( credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(), credentialInteractor.showTitleOnly ) { request, showTitleOnly -> - val flagEnabled = customBiometricPrompt() && constraintBp() + val flagEnabled = customBiometricPrompt() val showTitleOnlyForCredential = showTitleOnly && flagEnabled BiometricPromptHeaderViewModelImpl( request, @@ -82,8 +81,8 @@ constructor( val errorMessage: Flow<String> = combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p -> when (error) { - is CredentialStatus.Fail.Error -> error.error - ?: applicationContext.asBadCredentialErrorMessage(p) + is CredentialStatus.Fail.Error -> + error.error ?: applicationContext.asBadCredentialErrorMessage(p) is CredentialStatus.Fail.Throttled -> error.error null -> "" } 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 214420d45560..25d43d972fe2 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 @@ -36,7 +36,6 @@ import android.util.RotationUtils import android.view.HapticFeedbackConstants import android.view.MotionEvent import com.android.launcher3.icons.IconProvider -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.Utils.isSystem @@ -470,7 +469,7 @@ constructor( promptSelectorInteractor.prompt .map { when { - !(customBiometricPrompt() && constraintBp()) || it == null -> Pair(null, "") + !(customBiometricPrompt()) || it == null -> Pair(null, "") else -> context.getUserBadgedLogoInfo(it, iconProvider, activityTaskManager) } } @@ -487,7 +486,7 @@ constructor( /** Custom content view for the prompt. */ val contentView: Flow<PromptContentView?> = promptSelectorInteractor.prompt - .map { if (customBiometricPrompt() && constraintBp()) it?.contentView else null } + .map { if (customBiometricPrompt()) it?.contentView else null } .distinctUntilChanged() private val originalDescription = @@ -1045,7 +1044,7 @@ private fun BiometricPromptRequest.Biometric.getApplicationInfo( val packageName = when { componentNameForLogo != null -> componentNameForLogo.packageName - // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be + // TODO(b/353597496): We should check whether |allowBackgroundAuthentication| should be // removed. // This is being consistent with the check in [AuthController.showDialog()]. allowBackgroundAuthentication || isSystem(context, opPackageName) -> opPackageName diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt index 19ea007fc60f..c2a4ee36dec6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt @@ -28,7 +28,6 @@ import android.view.WindowManager import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY import com.airbnb.lottie.model.KeyPath -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor @@ -152,21 +151,6 @@ constructor( -> val topLeft = Point(sensorLocation.left, sensorLocation.top) - if (!constraintBp()) { - if (sensorLocation.isSensorVerticalInDefaultOrientation) { - if (displayRotation == DisplayRotation.ROTATION_0) { - topLeft.x -= bounds!!.width() - } else if (displayRotation == DisplayRotation.ROTATION_270) { - topLeft.y -= bounds!!.height() - } - } else { - if (displayRotation == DisplayRotation.ROTATION_180) { - topLeft.y -= bounds!!.height() - } else if (displayRotation == DisplayRotation.ROTATION_270) { - topLeft.x -= bounds!!.width() - } - } - } defaultOverlayViewParams.apply { x = topLeft.x y = topLeft.y diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 32731117a8f6..79f4568d73be 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -33,6 +33,7 @@ import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule; import com.android.systemui.keyboard.shortcut.ShortcutHelperModule; import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule; import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule; @@ -77,7 +78,7 @@ import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerIm import com.android.systemui.statusbar.policy.SensorPrivacyController; import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl; import com.android.systemui.toast.ToastModule; -import com.android.systemui.touchpad.tutorial.TouchpadKeyboardTutorialModule; +import com.android.systemui.touchpad.tutorial.TouchpadTutorialModule; import com.android.systemui.unfold.SysUIUnfoldStartableModule; import com.android.systemui.unfold.UnfoldTransitionModule; import com.android.systemui.util.kotlin.SysUICoroutinesModule; @@ -122,6 +123,7 @@ import javax.inject.Named; KeyboardShortcutsModule.class, KeyguardBlueprintModule.class, KeyguardSectionsModule.class, + KeyboardTouchpadTutorialModule.class, MediaModule.class, MediaMuteAwaitConnectionCli.StartableModule.class, MultiUserUtilsModule.class, @@ -142,7 +144,7 @@ import javax.inject.Named; SysUIUnfoldStartableModule.class, UnfoldTransitionModule.Startables.class, ToastModule.class, - TouchpadKeyboardTutorialModule.class, + TouchpadTutorialModule.class, VolumeModule.class, WallpaperModule.class, ShortcutHelperModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index b0f2c18db565..cbea87676d3a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -258,13 +258,6 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap - @ClassKey(KeyboardTouchpadTutorialCoreStartable::class) - abstract fun bindKeyboardTouchpadTutorialCoreStartable( - listener: KeyboardTouchpadTutorialCoreStartable - ): CoreStartable - - @Binds - @IntoMap @ClassKey(PhysicalKeyboardCoreStartable::class) abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt new file mode 100644 index 000000000000..8e6cb077a25e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial + +import android.app.Activity +import com.android.systemui.CoreStartable +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import dagger.Binds +import dagger.BindsOptionalOf +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface KeyboardTouchpadTutorialModule { + + @Binds + @IntoMap + @ClassKey(KeyboardTouchpadTutorialCoreStartable::class) + fun bindKeyboardTouchpadTutorialCoreStartable( + listener: KeyboardTouchpadTutorialCoreStartable + ): CoreStartable + + @Binds + @IntoMap + @ClassKey(KeyboardTouchpadTutorialActivity::class) + fun activity(impl: KeyboardTouchpadTutorialActivity): Activity + + // TouchpadModule dependencies below + // all should be optional to not introduce touchpad dependency in all sysui variants + + @BindsOptionalOf fun touchpadScreensProvider(): TouchpadTutorialScreensProvider + + @BindsOptionalOf fun touchpadGesturesInteractor(): TouchpadGesturesInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadKeyboardTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt index 8ba8db498a36..bd3e771f40bc 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadKeyboardTutorialModule.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt @@ -14,20 +14,13 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial +package com.android.systemui.inputdevice.tutorial -import android.app.Activity -import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity -import dagger.Binds -import dagger.Module -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap +import androidx.compose.runtime.Composable -@Module -interface TouchpadKeyboardTutorialModule { +interface TouchpadTutorialScreensProvider { - @Binds - @IntoMap - @ClassKey(TouchpadTutorialActivity::class) - fun activity(impl: TouchpadTutorialActivity): Activity + @Composable fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) + + @Composable fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt index f7f26314bb50..c5b0ca78d65a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.composable +package com.android.systemui.inputdevice.tutorial.ui.composable import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box @@ -34,9 +34,9 @@ import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import com.airbnb.lottie.compose.rememberLottieDynamicProperties import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.FINISHED +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NOT_STARTED import com.android.systemui.res.R -import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED -import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED @Composable fun ActionKeyTutorialScreen( diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt index 2b7f6742e335..c50b7dc06265 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.composable +package com.android.systemui.inputdevice.tutorial.ui.composable import android.graphics.ColorFilter import android.graphics.PorterDuff @@ -60,9 +60,9 @@ import com.airbnb.lottie.compose.LottieDynamicProperty import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieDynamicProperty -import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED -import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.IN_PROGRESS -import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.FINISHED +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.IN_PROGRESS +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NOT_STARTED enum class TutorialActionState { NOT_STARTED, diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialComponents.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt index f2276c8be71d..01ad585019d2 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialComponents.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.composable +package com.android.systemui.inputdevice.tutorial.ui.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt index d76ceb9380cd..0406bb9e6fef 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.composable +package com.android.systemui.inputdevice.tutorial.ui.composable import androidx.annotation.RawRes import androidx.annotation.StringRes diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt new file mode 100644 index 000000000000..3e382d669e5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial.ui.view + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import com.android.compose.theme.PlatformTheme +import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel +import java.util.Optional +import javax.inject.Inject + +/** + * Activity for out of the box experience for keyboard and touchpad. Note that it's possible that + * either of them are actually not connected when this is launched + */ +class KeyboardTouchpadTutorialActivity +@Inject +constructor( + private val viewModelFactory: KeyboardTouchpadTutorialViewModel.Factory, + private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, +) : ComponentActivity() { + + private val vm by + viewModels<KeyboardTouchpadTutorialViewModel>(factoryProducer = { viewModelFactory }) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PlatformTheme { + KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) { finish() } + } + } + // required to handle 3+ fingers on touchpad + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + } + + override fun onResume() { + super.onResume() + vm.onOpened() + } + + override fun onPause() { + super.onPause() + vm.onClosed() + } +} + +@Composable +fun KeyboardTouchpadTutorialContainer( + vm: KeyboardTouchpadTutorialViewModel, + touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, + closeTutorial: () -> Unit +) {} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt new file mode 100644 index 000000000000..39b1ec0f0390 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import java.util.Optional +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class KeyboardTouchpadTutorialViewModel( + private val gesturesInteractor: Optional<TouchpadGesturesInteractor> +) : ViewModel() { + + private val _screen = MutableStateFlow(Screen.BACK_GESTURE) + val screen: StateFlow<Screen> = _screen + + fun goTo(screen: Screen) { + _screen.value = screen + } + + fun onOpened() { + gesturesInteractor.ifPresent { it.disableGestures() } + } + + fun onClosed() { + gesturesInteractor.ifPresent { it.enableGestures() } + } + + class Factory + @Inject + constructor(private val gesturesInteractor: Optional<TouchpadGesturesInteractor>) : + ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + return KeyboardTouchpadTutorialViewModel(gesturesInteractor) as T + } + } +} + +enum class Screen { + BACK_GESTURE, + HOME_GESTURE, + ACTION_KEY +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt index cfe64e269c95..9f46846f0d91 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt @@ -16,11 +16,6 @@ package com.android.systemui.inputdevice.tutorial.data.model -data class TutorialSchedulerInfo( - val keyboard: DeviceSchedulerInfo = DeviceSchedulerInfo(), - val touchpad: DeviceSchedulerInfo = DeviceSchedulerInfo() -) - data class DeviceSchedulerInfo(var isLaunched: Boolean = false, var connectTime: Long? = null) { val wasEverConnected: Boolean get() = connectTime != null diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt index 31ff01836428..b9b38954784e 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt @@ -25,21 +25,32 @@ import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo -import com.android.systemui.inputdevice.tutorial.data.model.TutorialSchedulerInfo import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @SysUISingleton class TutorialSchedulerRepository @Inject -constructor(@Application private val applicationContext: Context) { +constructor( + @Application private val applicationContext: Context, + @Background private val backgroundScope: CoroutineScope +) { private val Context.dataStore: DataStore<Preferences> by - preferencesDataStore(name = DATASTORE_NAME) + preferencesDataStore(name = DATASTORE_NAME, scope = backgroundScope) - suspend fun loadData(): TutorialSchedulerInfo { + suspend fun isLaunched(deviceType: DeviceType): Boolean = loadData()[deviceType]!!.isLaunched + + suspend fun wasEverConnected(deviceType: DeviceType): Boolean = + loadData()[deviceType]!!.wasEverConnected + + suspend fun connectTime(deviceType: DeviceType): Long = loadData()[deviceType]!!.connectTime!! + + private suspend fun loadData(): Map<DeviceType, DeviceSchedulerInfo> { return applicationContext.dataStore.data.map { pref -> getSchedulerInfo(pref) }.first() } @@ -51,10 +62,10 @@ constructor(@Application private val applicationContext: Context) { applicationContext.dataStore.edit { pref -> pref[getLaunchedKey(device)] = true } } - private fun getSchedulerInfo(pref: Preferences): TutorialSchedulerInfo { - return TutorialSchedulerInfo( - keyboard = getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD), - touchpad = getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD) + private fun getSchedulerInfo(pref: Preferences): Map<DeviceType, DeviceSchedulerInfo> { + return mapOf( + DeviceType.KEYBOARD to getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD), + DeviceType.TOUCHPAD to getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD) ) } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt index 05e104468f67..b3b8f21a4a4b 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt @@ -16,23 +16,25 @@ package com.android.systemui.inputdevice.tutorial.domain.interactor -import android.content.Context -import android.content.Intent +import android.os.SystemProperties +import android.util.Log import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.touchpad.data.repository.TouchpadRepository -import java.time.Duration import java.time.Instant import javax.inject.Inject +import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** @@ -43,62 +45,72 @@ import kotlinx.coroutines.launch class TutorialSchedulerInteractor @Inject constructor( - @Application private val context: Context, - @Application private val applicationScope: CoroutineScope, - private val keyboardRepository: KeyboardRepository, - private val touchpadRepository: TouchpadRepository, - private val tutorialSchedulerRepository: TutorialSchedulerRepository + @Background private val backgroundScope: CoroutineScope, + keyboardRepository: KeyboardRepository, + touchpadRepository: TouchpadRepository, + private val repo: TutorialSchedulerRepository ) { + private val isAnyDeviceConnected = + mapOf( + KEYBOARD to keyboardRepository.isAnyKeyboardConnected, + TOUCHPAD to touchpadRepository.isAnyTouchpadConnected + ) + fun start() { - applicationScope.launch { - val info = tutorialSchedulerRepository.loadData() - if (!info.keyboard.isLaunched) { - applicationScope.launch { - schedule( - keyboardRepository.isAnyKeyboardConnected, - info.keyboard, - DeviceType.KEYBOARD - ) - } - } - if (!info.touchpad.isLaunched) { - applicationScope.launch { - schedule( - touchpadRepository.isAnyTouchpadConnected, - info.touchpad, - DeviceType.TOUCHPAD - ) - } + backgroundScope.launch { + // Merging two flows to ensure that launch tutorial is launched consecutively in order + // to avoid race condition + merge(touchpadScheduleFlow, keyboardScheduleFlow).collect { + val tutorialType = resolveTutorialType(it) + launchTutorial(tutorialType) } } } - private suspend fun schedule( - isAnyDeviceConnected: Flow<Boolean>, - info: DeviceSchedulerInfo, - deviceType: DeviceType - ) { - if (!info.wasEverConnected) { - waitForDeviceConnection(isAnyDeviceConnected) - info.connectTime = Instant.now().toEpochMilli() - tutorialSchedulerRepository.updateConnectTime(deviceType, info.connectTime!!) + private val touchpadScheduleFlow = flow { + if (!repo.isLaunched(TOUCHPAD)) { + schedule(TOUCHPAD) + emit(TOUCHPAD) } - delay(remainingTimeMillis(info.connectTime!!)) - waitForDeviceConnection(isAnyDeviceConnected) - info.isLaunched = true - tutorialSchedulerRepository.updateLaunch(deviceType) - launchTutorial() } - private suspend fun waitForDeviceConnection(isAnyDeviceConnected: Flow<Boolean>): Boolean { - return isAnyDeviceConnected.filter { it }.first() + private val keyboardScheduleFlow = flow { + if (!repo.isLaunched(KEYBOARD)) { + schedule(KEYBOARD) + emit(KEYBOARD) + } } - private fun launchTutorial() { - val intent = Intent(TUTORIAL_ACTION) - intent.addCategory(Intent.CATEGORY_DEFAULT) - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) + private suspend fun schedule(deviceType: DeviceType) { + if (!repo.wasEverConnected(deviceType)) { + waitForDeviceConnection(deviceType) + repo.updateConnectTime(deviceType, Instant.now().toEpochMilli()) + } + delay(remainingTimeMillis(start = repo.connectTime(deviceType))) + waitForDeviceConnection(deviceType) + } + + private suspend fun waitForDeviceConnection(deviceType: DeviceType) = + isAnyDeviceConnected[deviceType]!!.filter { it }.first() + + private suspend fun launchTutorial(tutorialType: TutorialType) { + if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH) + repo.updateLaunch(KEYBOARD) + if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH) + repo.updateLaunch(TOUCHPAD) + // TODO: launch tutorial + Log.d(TAG, "Launch tutorial for $tutorialType") + } + + private suspend fun resolveTutorialType(deviceType: DeviceType): TutorialType { + // Resolve the type of tutorial depending on which device are connected when the tutorial is + // launched. E.g. when the keyboard is connected for [LAUNCH_DELAY], both keyboard and + // touchpad are connected, we launch the tutorial for both. + if (repo.isLaunched(deviceType)) return TutorialType.NONE + val otherDevice = if (deviceType == KEYBOARD) TOUCHPAD else KEYBOARD + val isOtherDeviceConnected = isAnyDeviceConnected[otherDevice]!!.first() + if (!repo.isLaunched(otherDevice) && isOtherDeviceConnected) return TutorialType.BOTH + return if (deviceType == KEYBOARD) TutorialType.KEYBOARD else TutorialType.TOUCHPAD } private fun remainingTimeMillis(start: Long): Long { @@ -107,7 +119,20 @@ constructor( } companion object { - const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL" - private val LAUNCH_DELAY = Duration.ofHours(72).toMillis() + const val TAG = "TutorialSchedulerInteractor" + private val DEFAULT_LAUNCH_DELAY = 72.hours.inWholeMilliseconds + private val LAUNCH_DELAY: Long + get() = + SystemProperties.getLong( + "persist.peripheral_tutorial_delay_ms", + DEFAULT_LAUNCH_DELAY + ) + } + + enum class TutorialType { + KEYBOARD, + TOUCHPAD, + BOTH, + NONE } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 3f9c98d6a5d4..17c5977fc80a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -42,6 +42,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.Flags.notifyPowerManagerUserActivityBackground; import static com.android.systemui.Flags.refactorGetCurrentUser; +import static com.android.systemui.Flags.relockWithPowerButtonImmediately; import static com.android.systemui.Flags.translucentOccludingActivityFix; import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS; @@ -477,6 +478,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mUnlockingAndWakingFromDream = false; private boolean mHideAnimationRun = false; private boolean mHideAnimationRunning = false; + private boolean mIsKeyguardExitAnimationCanceled = false; private SoundPool mLockSounds; private int mLockSoundId; @@ -1588,10 +1590,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled( mSelectedUserInteractor.getSelectedUserId()), - true /* forceCallbacks */); + true /* forceCallbacks */, "setupLocked - keyguard service enabled"); } else { // The system's keyguard is disabled or missing. - setShowingLocked(false /* showing */, true /* forceCallbacks */); + setShowingLocked(false /* showing */, true /* forceCallbacks */, + "setupLocked - keyguard service disabled"); } mKeyguardTransitions.register( @@ -2334,6 +2337,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.e(TAG, "doKeyguard: we're still showing, but going away. Re-show the " + "keyguard rather than short-circuiting and resetting."); } else { + // We're removing "reset" in the refactor - "resetting" the views will happen + // as a reaction to the root cause of the "reset" signal. + if (KeyguardWmStateRefactor.isEnabled()) { + return; + } + // It's already showing, and we're not trying to show it while the screen is // off. We can simply reset all of the views, but don't hide the bouncer in case // the user is currently interacting with it. @@ -2827,9 +2836,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, playSound(mTrustedSoundId); } - private void updateActivityLockScreenState(boolean showing, boolean aodShowing) { + private void updateActivityLockScreenState(boolean showing, boolean aodShowing, String reason) { mUiBgExecutor.execute(() -> { - Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ")"); + Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", " + + reason + ")"); if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. @@ -2889,7 +2899,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Force if we're showing in the middle of unlocking, to ensure we end up in the // correct state. - setShowingLocked(true, hidingOrGoingAway /* force */); + setShowingLocked(true, hidingOrGoingAway /* force */, "handleShowInner"); mHiding = false; if (!KeyguardWmStateRefactor.isEnabled()) { @@ -3061,15 +3071,14 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mHiding = true; mKeyguardGoingAwayRunnable.run(); } else { - Log.d(TAG, "Hiding keyguard while occluded. Just hide the keyguard view and exit."); - if (!KeyguardWmStateRefactor.isEnabled()) { mKeyguardViewControllerLazy.get().hide( mSystemClock.uptimeMillis() + mHideAnimation.getStartOffset(), mHideAnimation.getDuration()); } - onKeyguardExitFinished(); + onKeyguardExitFinished("Hiding keyguard while occluded. Just hide the keyguard " + + "view and exit."); } // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while @@ -3100,6 +3109,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime + " fadeoutDuration=" + fadeoutDuration); synchronized (KeyguardViewMediator.this) { + mIsKeyguardExitAnimationCanceled = false; // Tell ActivityManager that we canceled the keyguard animation if // handleStartKeyguardExitAnimation was called, but we're not hiding the keyguard, // unless we're animating the surface behind the keyguard and will be hiding the @@ -3119,7 +3129,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Slog.w(TAG, "Failed to call onAnimationFinished", e); } } - setShowingLocked(mShowing, true /* force */); + setShowingLocked(mShowing, true /* force */, + "handleStartKeyguardExitAnimation - canceled"); return; } mHiding = false; @@ -3143,9 +3154,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Slog.w(TAG, "Failed to call onAnimationFinished", e); } } - onKeyguardExitFinished(); - mKeyguardViewControllerLazy.get().hide(0 /* startTime */, - 0 /* fadeoutDuration */); + if (!mIsKeyguardExitAnimationCanceled) { + onKeyguardExitFinished("onRemoteAnimationFinished"); + mKeyguardViewControllerLazy.get().hide(0 /* startTime */, + 0 /* fadeoutDuration */); + } mInteractionJankMonitor.end(CUJ_LOCKSCREEN_UNLOCK_ANIMATION); } @@ -3282,12 +3295,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, anim.start(); }); - onKeyguardExitFinished(); + onKeyguardExitFinished("remote animation disabled"); } } } - private void onKeyguardExitFinished() { + private void onKeyguardExitFinished(String reason) { if (DEBUG) Log.d(TAG, "onKeyguardExitFinished()"); // only play "unlock" noises if not on a call (since the incall UI // disables the keyguard) @@ -3295,7 +3308,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, playSounds(false); } - setShowingLocked(false); + setShowingLocked(false, "onKeyguardExitFinished: " + reason); mWakeAndUnlocking = false; mDismissCallbackRegistry.notifyDismissSucceeded(); resetKeyguardDonePendingLocked(); @@ -3343,6 +3356,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // A lock is pending, meaning the keyguard exit animation was cancelled because we're // re-locking. We should just end the surface-behind animation without exiting the // keyguard. The pending lock will be handled by onFinishedGoingToSleep(). + if (relockWithPowerButtonImmediately()) { + mIsKeyguardExitAnimationCanceled = true; + } finishSurfaceBehindRemoteAnimation(true /* showKeyguard */); maybeHandlePendingLock(); } else { @@ -3391,12 +3407,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, doKeyguardLocked(null); finishSurfaceBehindRemoteAnimation(true /* showKeyguard */); // Ensure WM is notified that we made a decision to show - setShowingLocked(true /* showing */, true /* force */); + setShowingLocked(true /* showing */, true /* force */, + "exitKeyguardAndFinishSurfaceBehindRemoteAnimation - relocked"); return; } - onKeyguardExitFinished(); + onKeyguardExitFinished("exitKeyguardAndFinishSurfaceBehindRemoteAnimation"); if (mKeyguardStateController.isDismissingFromSwipe() || wasShowing) { Log.d(TAG, "onKeyguardExitRemoteAnimationFinished" @@ -3453,7 +3470,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mSurfaceBehindRemoteAnimationRequested = false; mKeyguardStateController.notifyKeyguardGoingAway(false); if (mShowing) { - setShowingLocked(true, true); + setShowingLocked(true, true, "hideSurfaceBehindKeyguard"); } } @@ -3799,7 +3816,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // update lock screen state in ATMS here, otherwise ATMS tries to resume activities when // enabling doze state. if (mShowing || !mPendingLock || !mDozeParameters.canControlUnlockedScreenOff()) { - setShowingLocked(mShowing); + setShowingLocked(mShowing, "setDozing"); } } @@ -3809,7 +3826,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // is 1f), then show the activity lock screen. if (mAnimatingScreenOff && mDozing && linear == 1f) { mAnimatingScreenOff = false; - setShowingLocked(mShowing, true); + setShowingLocked(mShowing, true, "onDozeAmountChanged"); } } @@ -3847,11 +3864,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - void setShowingLocked(boolean showing) { - setShowingLocked(showing, false /* forceCallbacks */); + void setShowingLocked(boolean showing, String reason) { + setShowingLocked(showing, false /* forceCallbacks */, reason); } - private void setShowingLocked(boolean showing, boolean forceCallbacks) { + private void setShowingLocked(boolean showing, boolean forceCallbacks, String reason) { final boolean aodShowing = mDozing && !mWakeAndUnlocking; final boolean notifyDefaultDisplayCallbacks = showing != mShowing || forceCallbacks; final boolean updateActivityLockScreenState = showing != mShowing @@ -3862,9 +3879,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, notifyDefaultDisplayCallbacks(showing); } if (updateActivityLockScreenState) { - updateActivityLockScreenState(showing, aodShowing); + updateActivityLockScreenState(showing, aodShowing, reason); } - } private void notifyDefaultDisplayCallbacks(boolean showing) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index de60c1117c19..797a4ec419a9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -285,7 +285,7 @@ constructor( state: TransitionState ) { if (updateTransitionId != transitionId) { - Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId") return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt index 9dc77d3dc9d3..fb9719142b54 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt @@ -26,6 +26,7 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.scene.shared.flag.SceneContainerFlag import kotlinx.coroutines.ExperimentalCoroutinesApi @ExperimentalCoroutinesApi @@ -52,7 +53,11 @@ object AlternateBouncerUdfpsViewBinder { } } - launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } } + if (SceneContainerFlag.isEnabled) { + view.alpha = 1f + } else { + launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index a250b22dde07..91a7f7fc66bd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -37,13 +37,13 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel -import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scrim.ScrimView import dagger.Lazy import javax.inject.Inject @@ -67,7 +67,6 @@ constructor( private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>, private val windowManager: Lazy<WindowManager>, private val layoutInflater: Lazy<LayoutInflater>, - private val dismissCallbackRegistry: DismissCallbackRegistry, ) : CoreStartable { private val layoutParams: WindowManager.LayoutParams get() = @@ -95,9 +94,10 @@ constructor( private var alternateBouncerView: ConstraintLayout? = null override fun start() { - if (!DeviceEntryUdfpsRefactor.isEnabled) { + if (!DeviceEntryUdfpsRefactor.isEnabled || SceneContainerFlag.isEnabled) { return } + applicationScope.launch("$TAG#alternateBouncerWindowViewModel") { alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect { addAlternateBouncerWindowView -> @@ -110,7 +110,7 @@ constructor( bind(alternateBouncerView!!, alternateBouncerDependencies.get()) } else { removeViewFromWindowManager() - alternateBouncerDependencies.get().viewModel.hideAlternateBouncer() + alternateBouncerDependencies.get().viewModel.onRemovedFromWindow() } } } @@ -144,7 +144,7 @@ constructor( private val onAttachAddBackGestureHandler = object : View.OnAttachStateChangeListener { private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback { - onBackRequested() + alternateBouncerDependencies.get().viewModel.onBackRequested() } override fun onViewAttachedToWindow(view: View) { @@ -161,14 +161,12 @@ constructor( .findOnBackInvokedDispatcher() ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) } - - fun onBackRequested() { - alternateBouncerDependencies.get().viewModel.hideAlternateBouncer() - dismissCallbackRegistry.notifyDismissCancelled() - } } private fun addViewToWindowManager() { + if (SceneContainerFlag.isEnabled) { + return + } if (alternateBouncerView != null) { return } @@ -190,6 +188,7 @@ constructor( if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) { return } + optionallyAddUdfpsViews( view = view, udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel, @@ -202,12 +201,13 @@ constructor( viewModel = alternateBouncerDependencies.messageAreaViewModel, ) - val scrim = view.requireViewById(R.id.alternate_bouncer_scrim) as ScrimView + val scrim: ScrimView = view.requireViewById(R.id.alternate_bouncer_scrim) val viewModel = alternateBouncerDependencies.viewModel val swipeUpAnywhereGestureHandler = alternateBouncerDependencies.swipeUpAnywhereGestureHandler val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector - view.repeatWhenAttached { alternateBouncerViewContainer -> + + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch("$TAG#viewModel.registerForDismissGestures") { viewModel.registerForDismissGestures.collect { registerForDismissGestures -> @@ -216,11 +216,11 @@ constructor( swipeTag ) { _ -> alternateBouncerDependencies.powerInteractor.onUserTouch() - viewModel.showPrimaryBouncer() + viewModel.onTapped() } tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ -> alternateBouncerDependencies.powerInteractor.onUserTouch() - viewModel.showPrimaryBouncer() + viewModel.onTapped() } } else { swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt index df0b3dc3cbce..4908dbdec61e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shared.recents.utilities.Utilities.clamp import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,6 +51,7 @@ constructor( private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported val alpha: Flow<Float> = alternateBouncerViewModel.transitionToAlternateBouncerProgress.map { + SceneContainerFlag.assertInLegacyMode() clamp(it * 2f, 0f, 1f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt index 470f17b74032..7b0b23ffb2ff 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt @@ -18,15 +18,20 @@ package com.android.systemui.keyguard.ui.viewmodel import android.graphics.Color +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach @ExperimentalCoroutinesApi class AlternateBouncerViewModel @@ -34,12 +39,18 @@ class AlternateBouncerViewModel constructor( private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val dismissCallbackRegistry: DismissCallbackRegistry, + alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>, ) { // When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be: private val alternateBouncerScrimAlpha = .66f + /** Reports the alternate bouncer visible state if the scene container flag is enabled. */ + val isVisible: Flow<Boolean> = + alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.assertInNewMode() } + /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */ - val transitionToAlternateBouncerProgress = + val transitionToAlternateBouncerProgress: Flow<Float> = keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER) /** An observable for the scrim alpha. */ @@ -51,11 +62,16 @@ constructor( val registerForDismissGestures: Flow<Boolean> = transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged() - fun showPrimaryBouncer() { + fun onTapped() { statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) } - fun hideAlternateBouncer() { + fun onRemovedFromWindow() { + statusBarKeyguardViewManager.hideAlternateBouncer(false) + } + + fun onBackRequested() { statusBarKeyguardViewManager.hideAlternateBouncer(false) + dismissCallbackRegistry.notifyDismissCancelled() } } diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index 661da6d2af13..c2b5d98699b4 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -227,13 +227,33 @@ private fun inferTraceSectionName(): String { } /** + * Runs the given [block] in a new coroutine when `this` [View]'s Window's [WindowLifecycleState] is + * at least at [state] (or immediately after calling this function if the window is already at least + * at [state]), automatically canceling the work when the window is no longer at least at that + * state. + * + * [block] may be run multiple times, running once per every time this` [View]'s Window's + * [WindowLifecycleState] becomes at least at [state]. + */ +suspend fun View.repeatOnWindowLifecycle( + state: WindowLifecycleState, + block: suspend CoroutineScope.() -> Unit, +): Nothing { + when (state) { + WindowLifecycleState.ATTACHED -> repeatWhenAttachedToWindow(block) + WindowLifecycleState.VISIBLE -> repeatWhenWindowIsVisible(block) + WindowLifecycleState.FOCUSED -> repeatWhenWindowHasFocus(block) + } +} + +/** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this * function, if the view was already attached), automatically canceling the work when the view * becomes detached. * * Only use from the main thread. * - * The [block] may be run multiple times, running once per every time the view is attached. + * [block] may be run multiple times, running once per every time the view is attached. */ @MainThread suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing { @@ -249,7 +269,7 @@ suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> * * Only use from the main thread. * - * The [block] may be run multiple times, running once per every time the window becomes visible. + * [block] may be run multiple times, running once per every time the window becomes visible. */ @MainThread suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing { @@ -265,7 +285,7 @@ suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> U * * Only use from the main thread. * - * The [block] may be run multiple times, running once per every time the window is focused. + * [block] may be run multiple times, running once per every time the window is focused. */ @MainThread suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing { @@ -274,6 +294,21 @@ suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Un awaitCancellation() // satisfies return type of Nothing } +/** Lifecycle states for a [View]'s interaction with a [android.view.Window]. */ +enum class WindowLifecycleState { + /** Indicates that the [View] is attached to a [android.view.Window]. */ + ATTACHED, + /** + * Indicates that the [View] is attached to a [android.view.Window], and the window is visible. + */ + VISIBLE, + /** + * Indicates that the [View] is attached to a [android.view.Window], and the window is visible + * and focused. + */ + FOCUSED +} + private val View.isAttached get() = conflatedCallbackFlow { val onAttachListener = diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt index 0af5feaff3b2..77314813c34a 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt @@ -16,9 +16,10 @@ package com.android.systemui.lifecycle +import android.view.View import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** Base class for all System UI view-models. */ abstract class SysUiViewModel : SafeActivatable() { @@ -37,8 +38,20 @@ abstract class SysUiViewModel : SafeActivatable() { fun <T : SysUiViewModel> rememberViewModel( key: Any = Unit, factory: () -> T, -): T { - val instance = remember(key) { factory() } - LaunchedEffect(instance) { instance.activate() } - return instance -} +): T = rememberActivated(key, factory) + +/** + * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated + * whenever `this` [View]'s Window's [WindowLifecycleState] is at least at + * [minWindowLifecycleState], and is automatically canceled once that is no longer the case. + */ +suspend fun <T : SysUiViewModel> View.viewModel( + minWindowLifecycleState: WindowLifecycleState, + factory: () -> T, + block: suspend CoroutineScope.(T) -> Unit, +): Nothing = + repeatOnWindowLifecycle(minWindowLifecycleState) { + val instance = factory() + launch { instance.activate() } + block(instance) + } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 9939075b77d2..1511f31a3f92 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -259,6 +259,13 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { } /** + * @return height with the squishiness fraction applied. + */ + int getSquishedQqsHeight() { + return mHeader.getSquishedHeight(); + } + + /** * Returns the size of QS (or the QSCustomizer), regardless of the measured size of this view * @return size in pixels of QS (or QSCustomizer) */ @@ -267,6 +274,13 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { : mQSPanel.getMeasuredHeight(); } + /** + * @return height with the squishiness fraction applied. + */ + int getSquishedQsHeight() { + return mQSPanel.getSquishedHeight(); + } + public void setExpansion(float expansion) { mQsExpansion = expansion; if (mQSPanelContainer != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index a6fd35a9ee37..0b37b5b7be3d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -992,11 +992,25 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl return mContainer.getQqsHeight(); } + /** + * @return height with the squishiness fraction applied. + */ + public int getSquishedQqsHeight() { + return mContainer.getSquishedQqsHeight(); + } + public int getQSHeight() { return mContainer.getQsHeight(); } /** + * @return height with the squishiness fraction applied. + */ + public int getSquishedQsHeight() { + return mContainer.getSquishedQsHeight(); + } + + /** * Pass the size of the navbar when it's at the bottom of the device so it can be used as * padding * @param padding size of the bottom nav bar in px diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 032891fa715e..d3bed27ab2ab 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -733,6 +733,30 @@ public class QSPanel extends LinearLayout implements Tunable { mCanCollapse = canCollapse; } + /** + * @return height with the {@link QSPanel#setSquishinessFraction(float)} applied. + */ + public int getSquishedHeight() { + if (mFooter != null) { + final ViewGroup.LayoutParams footerLayoutParams = mFooter.getLayoutParams(); + final int footerBottomMargin; + if (footerLayoutParams instanceof MarginLayoutParams) { + footerBottomMargin = ((MarginLayoutParams) footerLayoutParams).bottomMargin; + } else { + footerBottomMargin = 0; + } + // This is the distance between the top of the QSPanel and the last view in the + // layout (which is the effective the bottom) + return mFooter.getBottom() + footerBottomMargin - getTop(); + } + if (mTileLayout != null) { + // Footer absence means that the panel is in the QQS. In this case it's just height + // of the tiles + paddings. + return mTileLayout.getTilesHeight() + getPaddingBottom() + getPaddingTop(); + } + return getHeight(); + } + @Nullable @VisibleForTesting View getMediaPlaceholder() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 5a3f1c0b7426..8fde52c910da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -123,4 +123,11 @@ public class QuickStatusBarHeader extends FrameLayout { lp.setMarginEnd(marginEnd); view.setLayoutParams(lp); } + + /** + * @return height with the squishiness fraction applied. + */ + public int getSquishedHeight() { + return mHeaderQsPanel.getSquishedHeight(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt index 9b8dba166274..9fb1d46c4241 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt @@ -37,19 +37,22 @@ constructor( override fun map(config: QSTileConfig, data: AirplaneModeTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - val icon = + iconRes = + if (data.isEnabled) { + R.drawable.qs_airplane_icon_on + } else { + R.drawable.qs_airplane_icon_off + } + + icon = { Icon.Loaded( resources.getDrawable( - if (data.isEnabled) { - R.drawable.qs_airplane_icon_on - } else { - R.drawable.qs_airplane_icon_off - }, + iconRes!!, theme, ), contentDescription = null ) - this.icon = { icon } + } if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = resources.getStringArray(R.array.tile_states_airplane)[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt index 875079cae8eb..984228d80b7f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt @@ -41,16 +41,25 @@ constructor( ) : QSTileDataToStateMapper<CustomTileDataModel> { override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState { - val userContext = context.createContextAsUser(UserHandle(data.user.identifier), 0) + val userContext = + try { + context.createContextAsUser(UserHandle(data.user.identifier), 0) + } catch (exception: IllegalStateException) { + null + } val iconResult = - getIconProvider( - userContext = userContext, - icon = data.tile.icon, - callingAppUid = data.callingAppUid, - packageName = data.componentName.packageName, - defaultIcon = data.defaultTileIcon, - ) + if (userContext != null) { + getIconProvider( + userContext = userContext, + icon = data.tile.icon, + callingAppUid = data.callingAppUid, + packageName = data.componentName.packageName, + defaultIcon = data.defaultTileIcon, + ) + } else { + IconResult({ null }, true) + } return QSTileState.build(iconResult.iconProvider, data.tile.label) { var tileState: Int = data.tile.state @@ -61,7 +70,7 @@ constructor( icon = iconResult.iconProvider activationState = if (iconResult.failedToLoad) { - QSTileState.ActivationState.INACTIVE + QSTileState.ActivationState.UNAVAILABLE } else { QSTileState.ActivationState.valueOf(tileState) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index eec5d3d915f8..204ead3fe29c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -79,6 +79,7 @@ constructor( flowOf( InternetTileModel.Active( secondaryTitle = secondary, + iconId = wifiIcon.icon.res, icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null), stateDescription = wifiIcon.contentDescription, contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt index ae2f32aae874..dfcf21628c3b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt @@ -34,7 +34,6 @@ import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.qs.QSContainerImpl import com.android.systemui.qs.QSImpl import com.android.systemui.qs.dagger.QSSceneComponent -import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel.state import com.android.systemui.res.R import com.android.systemui.settings.brightness.MirrorController import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -126,12 +125,18 @@ interface QSSceneAdapter { /** The current height of QQS in the current [qsView], or 0 if there's no view. */ val qqsHeight: Int + /** @return height with the squishiness fraction applied. */ + val squishedQqsHeight: Int + /** * The current height of QS in the current [qsView], or 0 if there's no view. If customizing, it * will return the height allocated to the customizer. */ val qsHeight: Int + /** @return height with the squishiness fraction applied. */ + val squishedQsHeight: Int + /** Compatibility for use by LockscreenShadeTransitionController. Matches default from [QS] */ val isQsFullyCollapsed: Boolean get() = true @@ -273,9 +278,15 @@ constructor( override val qqsHeight: Int get() = qsImpl.value?.qqsHeight ?: 0 + override val squishedQqsHeight: Int + get() = qsImpl.value?.squishedQqsHeight ?: 0 + override val qsHeight: Int get() = qsImpl.value?.qsHeight ?: 0 + override val squishedQsHeight: Int + get() = qsImpl.value?.squishedQsHeight ?: 0 + // If value is null, there's no QS and therefore it's fully collapsed. override val isQsFullyCollapsed: Boolean get() = qsImpl.value?.isFullyCollapsed ?: true diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 5b5013352c29..3ec088c66e10 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.CoreStartable import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.logging.BouncerUiEvent @@ -132,6 +133,7 @@ constructor( private val keyguardEnabledInteractor: KeyguardEnabledInteractor, private val dismissCallbackRegistry: DismissCallbackRegistry, private val statusBarStateController: SysuiStatusBarStateController, + private val alternateBouncerInteractor: AlternateBouncerInteractor, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -152,6 +154,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCallbacks() refreshLockscreenEnabled() + handleHideAlternateBouncerOnTransitionToGone() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -228,13 +231,16 @@ constructor( }, headsUpInteractor.isHeadsUpOrAnimatingAway, occlusionInteractor.invisibleDueToOcclusion, + alternateBouncerInteractor.isVisible, ) { visibilityForTransitionState, isHeadsUpOrAnimatingAway, invisibleDueToOcclusion, + isAlternateBouncerVisible, -> when { isHeadsUpOrAnimatingAway -> true to "showing a HUN" + isAlternateBouncerVisible -> true to "showing alternate bouncer" invisibleDueToOcclusion -> false to "invisible due to occlusion" else -> visibilityForTransitionState } @@ -351,7 +357,9 @@ constructor( ) } val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen) - val isOnBouncer = renderedScenes.contains(Scenes.Bouncer) + val isOnBouncer = + renderedScenes.contains(Scenes.Bouncer) || + alternateBouncerInteractor.isVisibleState() if (!deviceUnlockStatus.isUnlocked) { return@mapNotNull if (isOnLockscreen || isOnBouncer) { // Already on lockscreen or bouncer, no need to change scenes. @@ -379,12 +387,13 @@ constructor( !statusBarStateController.leaveOpenOnKeyguardHide() ) { Scenes.Gone to - "device was unlocked in Bouncer scene and shade" + + "device was unlocked with bouncer showing and shade" + " didn't need to be left open" } else { val prevScene = previousScene.value (prevScene ?: Scenes.Gone) to - "device was unlocked in Bouncer scene, from sceneKey=$prevScene" + "device was unlocked with bouncer showing," + + " from sceneKey=$prevScene" } isOnLockscreen -> // The lockscreen should be dismissed automatically in 2 scenarios: @@ -776,4 +785,14 @@ constructor( .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() } } } + + private fun handleHideAlternateBouncerOnTransitionToGone() { + applicationScope.launch { + sceneInteractor.transitionState + .map { it.isIdle(Scenes.Gone) } + .distinctUntilChanged() + .filter { it } + .collectLatest { alternateBouncerInteractor.hide() } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index bccbb1130bcc..f6924f222e11 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -5,15 +5,18 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.WindowInsets +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow /** A root view of the main SysUI window that supports scenes. */ +@ExperimentalCoroutinesApi class SceneWindowRootView( context: Context, attrs: AttributeSet?, @@ -35,6 +38,7 @@ class SceneWindowRootView( scenes: Set<Scene>, layoutInsetController: LayoutInsetsController, sceneDataSourceDelegator: SceneDataSourceDelegator, + alternateBouncerDependencies: AlternateBouncerDependencies, ) { this.viewModel = viewModel setLayoutInsetsController(layoutInsetController) @@ -49,6 +53,7 @@ class SceneWindowRootView( super.setVisibility(if (isVisible) View.VISIBLE else View.INVISIBLE) }, dataSourceDelegator = sceneDataSourceDelegator, + alternateBouncerDependencies = alternateBouncerDependencies, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index d31d6f4137c1..73a8e4c24578 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -37,6 +37,8 @@ import com.android.internal.policy.ScreenDecorationsUtils 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.ScreenDecorProvider +import com.android.systemui.keyguard.ui.composable.AlternateBouncer +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -48,12 +50,14 @@ import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +@ExperimentalCoroutinesApi object SceneWindowRootViewBinder { /** Binds between the view and view-model pertaining to a specific scene container. */ @@ -66,6 +70,7 @@ object SceneWindowRootViewBinder { scenes: Set<Scene>, onVisibilityChangedInternal: (isVisible: Boolean) -> Unit, dataSourceDelegator: SceneDataSourceDelegator, + alternateBouncerDependencies: AlternateBouncerDependencies, ) { val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key } val sortedSceneByKey: Map<SceneKey, Scene> = buildMap { @@ -120,6 +125,14 @@ object SceneWindowRootViewBinder { sharedNotificationContainer ) view.addView(sharedNotificationContainer) + + // TODO (b/358354906): use an overlay for the alternate bouncer + view.addView( + createAlternateBouncerView( + context = view.context, + alternateBouncerDependencies = alternateBouncerDependencies, + ) + ) } launch { @@ -164,6 +177,19 @@ object SceneWindowRootViewBinder { } } + private fun createAlternateBouncerView( + context: Context, + alternateBouncerDependencies: AlternateBouncerDependencies, + ): ComposeView { + return ComposeView(context).apply { + setContent { + AlternateBouncer( + alternateBouncerDependencies = alternateBouncerDependencies, + ) + } + } + } + // TODO(b/298525212): remove once Compose exposes window inset bounds. private fun displayCutoutFromWindowInsets( scope: CoroutineScope, diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index b54bf6ca9ef8..46ac54f63183 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -270,15 +270,18 @@ class ScreenRecordPermissionDialogDelegate( return listOf( ScreenShareOption( SINGLE_APP, - R.string.screen_share_permission_dialog_option_single_app, + R.string.screenrecord_permission_dialog_option_text_single_app, R.string.screenrecord_permission_dialog_warning_single_app, - startButtonText = R.string.screenrecord_permission_dialog_continue, + startButtonText = + R.string + .media_projection_entry_generic_permission_dialog_continue_single_app, ), ScreenShareOption( ENTIRE_SCREEN, - R.string.screen_share_permission_dialog_option_entire_screen, + R.string.screenrecord_permission_dialog_option_text_entire_screen, R.string.screenrecord_permission_dialog_warning_entire_screen, - startButtonText = R.string.screenrecord_permission_dialog_continue, + startButtonText = + R.string.screenrecord_permission_dialog_continue_entire_screen, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index bc2377895101..21bbaa5a41f2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.ui.view.KeyguardRootView +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -83,6 +84,7 @@ abstract class ShadeViewProviderModule { scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>, layoutInsetController: NotificationInsetsController, sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>, + alternateBouncerDependencies: Provider<AlternateBouncerDependencies>, ): WindowRootView { return if (SceneContainerFlag.isEnabled) { checkNoSceneDuplicates(scenesProvider.get()) @@ -96,6 +98,7 @@ abstract class ShadeViewProviderModule { scenes = scenesProvider.get(), layoutInsetController = layoutInsetController, sceneDataSourceDelegator = sceneDataSourceDelegator.get(), + alternateBouncerDependencies = alternateBouncerDependencies.get(), ) sceneWindowRootView } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index fd08e898fce3..a30b8772c3d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -17,14 +17,14 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.util.Log -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.view.onLayoutChanged import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.lifecycle.viewModel import com.android.systemui.res.R import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationScrollViewModel @@ -33,7 +33,6 @@ import com.android.systemui.util.kotlin.launchAndDispose import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -46,7 +45,7 @@ constructor( dumpManager: DumpManager, @Main private val mainImmediateDispatcher: CoroutineDispatcher, private val view: NotificationScrollView, - private val viewModel: NotificationScrollViewModel, + private val viewModelFactory: NotificationScrollViewModel.Factory, private val configuration: ConfigurationState, ) : FlowDumperImpl(dumpManager) { @@ -61,38 +60,42 @@ constructor( } fun bindWhileAttached(): DisposableHandle { - return view.asView().repeatWhenAttached(mainImmediateDispatcher) { - repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } - } + return view.asView().repeatWhenAttached(mainImmediateDispatcher) { bind() } } - suspend fun bind() = coroutineScope { - launchAndDispose { - updateViewPosition() - view.asView().onLayoutChanged { updateViewPosition() } - } + suspend fun bind(): Nothing = + view.asView().viewModel( + minWindowLifecycleState = WindowLifecycleState.ATTACHED, + factory = viewModelFactory::create, + ) { viewModel -> + launchAndDispose { + updateViewPosition() + view.asView().onLayoutChanged { updateViewPosition() } + } - launch { - viewModel - .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset) - .collect { view.setScrimClippingShape(it) } - } + launch { + viewModel + .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset) + .collect { view.setScrimClippingShape(it) } + } - launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } } - launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } } - launch { viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } } - launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } - launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } } + launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } } + launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } } + launch { + viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } + } + launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } + launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } } - launchAndDispose { - view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) - view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) - DisposableHandle { - view.setSyntheticScrollConsumer(null) - view.setCurrentGestureOverscrollConsumer(null) + launchAndDispose { + view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) + view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) + DisposableHandle { + view.setSyntheticScrollConsumer(null) + view.setCurrentGestureOverscrollConsumer(null) + } } } - } /** flow of the scrim clipping radius */ private val scrimRadius: Flow<Int> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 2ba79a8612bb..bfb624a9287b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneFamilies @@ -33,9 +33,11 @@ import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrim import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA -import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.ActivatableFlowDumper +import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl import dagger.Lazy -import javax.inject.Inject +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -43,9 +45,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ -@SysUISingleton class NotificationScrollViewModel -@Inject +@AssistedInject constructor( dumpManager: DumpManager, stackAppearanceInteractor: NotificationStackAppearanceInteractor, @@ -54,7 +55,14 @@ constructor( // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released - // while the flag is off, creating this object too early results in a crash keyguardInteractor: Lazy<KeyguardInteractor>, -) : FlowDumperImpl(dumpManager) { +) : + ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"), + SysUiViewModel() { + + override suspend fun onActivated() { + activateFlowDumper() + } + /** * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while @@ -186,4 +194,9 @@ constructor( keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing") } } + + @AssistedFactory + interface Factory { + fun create(): NotificationScrollViewModel + } } 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 db8cd575f77b..c4fbc37b2dd5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -30,6 +30,7 @@ import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; import static com.android.systemui.Flags.keyboardShortcutHelperRewrite; import static com.android.systemui.Flags.lightRevealMigration; import static com.android.systemui.Flags.newAodTransition; +import static com.android.systemui.Flags.relockWithPowerButtonImmediately; import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL; import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; @@ -2352,8 +2353,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } else if (mState == StatusBarState.KEYGUARD && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing() && mStatusBarKeyguardViewManager.isSecure()) { - Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer"); - mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); + if (!relockWithPowerButtonImmediately()) { + Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer"); + if (SceneContainerFlag.isEnabled()) { + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); + } else { + mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index f8f9b77c2b22..5486abba9987 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -972,7 +972,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (isOccluded && !mDozing) { mCentralSurfaces.hideKeyguard(); if (hideBouncerWhenShowing || needsFullscreenBouncer()) { - hideBouncer(false /* destroyView */); + // We're removing "reset" in the refactor - bouncer will be hidden by the root + // cause of the "reset" calls. + if (!KeyguardWmStateRefactor.isEnabled()) { + hideBouncer(false /* destroyView */); + } } } else { showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index 4368239c31f0..bd6a1c05ddc9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -227,6 +227,15 @@ constructor( callNotificationInfo // This shouldn't happen, but protect against it in case ?: return OngoingCallModel.NoCall + logger.log( + TAG, + LogLevel.DEBUG, + { + bool1 = Flags.statusBarCallChipNotificationIcon() + bool2 = currentInfo.notificationIconView != null + }, + { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" } + ) val icon = if (Flags.statusBarCallChipNotificationIcon()) { currentInfo.notificationIconView @@ -257,6 +266,7 @@ constructor( private fun updateInfoFromNotifModel(notifModel: ActiveNotificationModel?) { if (notifModel == null) { + logger.log(TAG, LogLevel.DEBUG, {}, { "NotifInteractorCallModel: null" }) removeChip() } else if (notifModel.callType != CallType.Ongoing) { logger.log( @@ -267,6 +277,18 @@ constructor( ) removeChip() } else { + logger.log( + TAG, + LogLevel.DEBUG, + { + str1 = notifModel.key + long1 = notifModel.whenTime + str1 = notifModel.callType.name + bool1 = notifModel.statusBarChipIconView != null + }, + { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" } + ) + val newOngoingCallInfo = CallNotificationInfo( notifModel.key, diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt new file mode 100644 index 000000000000..238e8a1c01ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.touchpad.tutorial + +import android.app.Activity +import androidx.compose.runtime.Composable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider +import com.android.systemui.model.SysUiState +import com.android.systemui.settings.DisplayTracker +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen +import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen +import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import kotlinx.coroutines.CoroutineScope + +@Module +interface TouchpadTutorialModule { + + @Binds + @IntoMap + @ClassKey(TouchpadTutorialActivity::class) + fun activity(impl: TouchpadTutorialActivity): Activity + + companion object { + @Provides + fun touchpadScreensProvider(): TouchpadTutorialScreensProvider { + return ScreensProvider + } + + @SysUISingleton + @Provides + fun touchpadGesturesInteractor( + sysUiState: SysUiState, + displayTracker: DisplayTracker, + @Background backgroundScope: CoroutineScope + ): TouchpadGesturesInteractor { + return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope) + } + } +} + +private object ScreensProvider : TouchpadTutorialScreensProvider { + @Composable + override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { + BackGestureTutorialScreen(onDoneButtonClicked, onBack) + } + + @Composable + override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { + HomeGestureTutorialScreen(onDoneButtonClicked, onBack) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt index b6c2ae794da9..df95232758a4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt @@ -16,22 +16,16 @@ package com.android.systemui.touchpad.tutorial.domain.interactor -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.model.SysUiState import com.android.systemui.settings.DisplayTracker import com.android.systemui.shared.system.QuickStepContract -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -@SysUISingleton -class TouchpadGesturesInteractor -@Inject -constructor( +class TouchpadGesturesInteractor( private val sysUiState: SysUiState, private val displayTracker: DisplayTracker, - @Background private val backgroundScope: CoroutineScope + private val backgroundScope: CoroutineScope ) { fun disableGestures() { setGesturesState(disabled = true) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 5980e1de85b4..1c8041ff5b31 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.airbnb.lottie.compose.rememberLottieDynamicProperties import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig +import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index dfe9c0df9ab7..57d7c84af4ba 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -28,6 +28,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext +import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index ed3110c04131..0a6283aa7417 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.airbnb.lottie.compose.rememberLottieDynamicProperties import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig +import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 14355fa18335..65b452a81da8 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton import com.android.systemui.res.R @Composable diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index 48e6397a0a42..256c5b590b14 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme -import com.android.systemui.touchpad.tutorial.ui.composable.ActionKeyTutorialScreen +import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.TutorialSelectionScreen diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java index 56b466249e93..32f2ca6fb696 100644 --- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -116,7 +116,7 @@ public class CreateUserActivity extends Activity { return mCreateUserDialogController.createDialog( this, this::startActivity, - (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin() + (mUserCreator.canCreateAdminUser() && mUserCreator.isUserAdmin() && !isKeyguardShowing), this::addUserNow, this::finish diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt index 9304a462b6a8..a426da929d6b 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt @@ -19,6 +19,7 @@ import android.app.Dialog import android.content.Context import android.content.pm.UserInfo import android.graphics.drawable.Drawable +import android.multiuser.Flags import android.os.UserManager import com.android.internal.util.UserIcons import com.android.settingslib.users.UserCreatingDialog @@ -91,7 +92,17 @@ constructor( return userManager.isAdminUser } - fun isMultipleAdminEnabled(): Boolean { - return UserManager.isMultipleAdminEnabled() + /** + * Checks if the creation of a new admin user is allowed. + * + * @return `true` if creating a new admin is allowed, `false` otherwise. + */ + fun canCreateAdminUser(): Boolean { + return if (Flags.unicornModeRefactoringForHsumReadOnly()) { + UserManager.isMultipleAdminEnabled() && + !userManager.hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN) + } else { + UserManager.isMultipleAdminEnabled() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt index e17274c435aa..ade6c3df2e0f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt @@ -19,11 +19,14 @@ package com.android.systemui.util.kotlin import android.util.IndentingPrintWriter import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager +import com.android.systemui.lifecycle.SafeActivatable +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.util.asIndenting import com.android.systemui.util.printCollection import java.io.PrintWriter import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -64,19 +67,20 @@ interface FlowDumper : Dumpable { } /** - * An implementation of [FlowDumper]. This be extended directly, or can be used to implement - * [FlowDumper] by delegation. - * - * @param dumpManager if provided, this will be used by the [FlowDumperImpl] to register and - * unregister itself when there is something to dump. - * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this - * class's name will be used. If you're implementing by delegation, you probably want to provide - * this tag to get a meaningful dumpable name. + * The minimal implementation of FlowDumper. The owner must either register this with the + * DumpManager, or else call [dumpFlows] from its own [Dumpable.dump] method. */ -open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = null) : FlowDumper { +open class SimpleFlowDumper : FlowDumper { + private val stateFlowMap = ConcurrentHashMap<String, StateFlow<*>>() private val sharedFlowMap = ConcurrentHashMap<String, SharedFlow<*>>() private val flowCollectionMap = ConcurrentHashMap<Pair<String, String>, Any>() + + protected fun isNotEmpty(): Boolean = + stateFlowMap.isNotEmpty() || sharedFlowMap.isNotEmpty() || flowCollectionMap.isNotEmpty() + + protected open fun onMapKeysChanged(added: Boolean) {} + override fun dumpFlows(pw: IndentingPrintWriter) { pw.printCollection("StateFlow (value)", stateFlowMap.toSortedMap().entries) { (key, flow) -> append(key).append('=').println(flow.value) @@ -92,43 +96,62 @@ open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = } } - private val Any.idString: String - get() = Integer.toHexString(System.identityHashCode(this)) - override fun <T> Flow<T>.dumpWhileCollecting(dumpName: String): Flow<T> = flow { val mapKey = dumpName to idString try { collect { flowCollectionMap[mapKey] = it ?: "null" - updateRegistration(required = true) + onMapKeysChanged(added = true) emit(it) } } finally { flowCollectionMap.remove(mapKey) - updateRegistration(required = false) + onMapKeysChanged(added = false) } } override fun <T, F : StateFlow<T>> F.dumpValue(dumpName: String): F { stateFlowMap[dumpName] = this + onMapKeysChanged(added = true) return this } override fun <T, F : SharedFlow<T>> F.dumpReplayCache(dumpName: String): F { sharedFlowMap[dumpName] = this + onMapKeysChanged(added = true) return this } - private val dumpManagerName = tag ?: "[$idString] ${javaClass.simpleName}" + protected val Any.idString: String + get() = Integer.toHexString(System.identityHashCode(this)) +} + +/** + * An implementation of [FlowDumper] that registers itself whenever there is something to dump. This + * class is meant to be extended. + * + * @param dumpManager this will be used by the [FlowDumperImpl] to register and unregister itself + * when there is something to dump. + * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this + * class's name will be used. + */ +abstract class FlowDumperImpl( + private val dumpManager: DumpManager, + private val tag: String? = null, +) : SimpleFlowDumper() { + + override fun onMapKeysChanged(added: Boolean) { + updateRegistration(required = added) + } + + private val dumpManagerName = "[$idString] ${tag ?: javaClass.simpleName}" + private var registered = AtomicBoolean(false) + private fun updateRegistration(required: Boolean) { - if (dumpManager == null) return if (required && registered.get()) return synchronized(registered) { - val shouldRegister = - stateFlowMap.isNotEmpty() || - sharedFlowMap.isNotEmpty() || - flowCollectionMap.isNotEmpty() + val shouldRegister = isNotEmpty() val wasRegistered = registered.getAndSet(shouldRegister) if (wasRegistered != shouldRegister) { if (shouldRegister) { @@ -140,3 +163,49 @@ open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = } } } + +/** + * A [FlowDumper] that also has an [activateFlowDumper] suspend function that allows the dumper to + * be registered with the [DumpManager] only when activated, just like + * [Activatable.activate()][com.android.systemui.lifecycle.Activatable.activate]. + */ +interface ActivatableFlowDumper : FlowDumper { + suspend fun activateFlowDumper() +} + +/** + * Implementation of [ActivatableFlowDumper] that only registers when activated. + * + * This is generally used to implement [ActivatableFlowDumper] by delegation, especially for + * [SysUiViewModel] implementations. + * + * @param dumpManager used to automatically register and unregister this instance when activated and + * there is something to dump. + * @param tag the name with which this is dumper registered. + */ +class ActivatableFlowDumperImpl( + private val dumpManager: DumpManager, + tag: String, +) : SimpleFlowDumper(), ActivatableFlowDumper { + + private val registration = + object : SafeActivatable() { + override suspend fun onActivated() { + try { + dumpManager.registerCriticalDumpable( + dumpManagerName, + this@ActivatableFlowDumperImpl + ) + awaitCancellation() + } finally { + dumpManager.unregisterDumpable(dumpManagerName) + } + } + } + + private val dumpManagerName = "[$idString] $tag" + + override suspend fun activateFlowDumper() { + registration.activate() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index 5d8b6f144d97..d39daafd2311 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -79,13 +79,15 @@ interface AudioModule { localBluetoothManager: LocalBluetoothManager?, @Application coroutineScope: CoroutineScope, @Background coroutineContext: CoroutineContext, + volumeLogger: VolumeLogger ): AudioSharingRepository = if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { AudioSharingRepositoryImpl( contentResolver, localBluetoothManager, coroutineScope, - coroutineContext + coroutineContext, + volumeLogger ) } else { AudioSharingRepositoryEmptyImpl() diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt index e46ce2699beb..24fb001a1b6d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.data.repository import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState import java.io.PrintWriter import javax.inject.Inject @@ -27,10 +28,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -private const val TAG = "VolumePanelGlobalState" +private const val TAG = "VolumePanelGlobalStateRepository" @SysUISingleton -class VolumePanelGlobalStateRepository @Inject constructor(dumpManager: DumpManager) : Dumpable { +class VolumePanelGlobalStateRepository +@Inject +constructor( + dumpManager: DumpManager, + private val logger: VolumePanelLogger, +) : Dumpable { private val mutableGlobalState = MutableStateFlow( @@ -48,6 +54,7 @@ class VolumePanelGlobalStateRepository @Inject constructor(dumpManager: DumpMana update: (currentState: VolumePanelGlobalState) -> VolumePanelGlobalState ) { mutableGlobalState.update(update) + logger.onVolumePanelGlobalStateChanged(mutableGlobalState.value) } override fun dump(pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt index 5301b008bab7..9de862a814d6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.domain.interactor import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.model.ComponentModel +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import javax.inject.Inject import javax.inject.Provider @@ -26,8 +27,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn interface ComponentsInteractor { @@ -45,6 +50,7 @@ constructor( enabledComponents: Collection<VolumePanelComponentKey>, defaultCriteria: Provider<ComponentAvailabilityCriteria>, @VolumePanelScope coroutineScope: CoroutineScope, + private val logger: VolumePanelLogger, private val criteriaByKey: Map< VolumePanelComponentKey, @@ -57,12 +63,18 @@ constructor( combine( enabledComponents.map { componentKey -> val componentCriteria = (criteriaByKey[componentKey] ?: defaultCriteria).get() - componentCriteria.isAvailable().map { isAvailable -> - ComponentModel(componentKey, isAvailable = isAvailable) - } + componentCriteria + .isAvailable() + .distinctUntilChanged() + .conflate() + .onEach { logger.onComponentAvailabilityChanged(componentKey, it) } + .map { isAvailable -> + ComponentModel(componentKey, isAvailable = isAvailable) + } } ) { it.asList() } - .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt index cc513b5d820c..276326cbf430 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt @@ -20,15 +20,41 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.VolumeLog -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import javax.inject.Inject private const val TAG = "SysUI_VolumePanel" /** Logs events related to the Volume Panel. */ -@VolumePanelScope class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) { + fun onVolumePanelStateChanged(state: VolumePanelState) { + logBuffer.log(TAG, LogLevel.DEBUG, { str1 = state.toString() }, { "State changed: $str1" }) + } + + fun onComponentAvailabilityChanged(key: VolumePanelComponentKey, isAvailable: Boolean) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = isAvailable + }, + { "$str1 isAvailable=$bool1" } + ) + } + + fun onVolumePanelGlobalStateChanged(globalState: VolumePanelGlobalState) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = globalState.isVisible }, + { "Global state changed: isVisible=$bool1" } + ) + } + fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) { logBuffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt index 1c51236689d8..a06d3e3c6785 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.ui.layout import com.android.systemui.volume.panel.ui.viewmodel.ComponentState +import com.android.systemui.volume.panel.ui.viewmodel.toLogString /** Represents components grouping into the layout. */ data class ComponentsLayout( @@ -29,3 +30,12 @@ data class ComponentsLayout( /** This is a separated entity that is always visible on the bottom of the Volume Panel. */ val bottomBarComponent: ComponentState, ) + +fun ComponentsLayout.toLogString(): String { + return "(" + + " headerComponents=${headerComponents.joinToString { it.toLogString() }}" + + " contentComponents=${contentComponents.joinToString { it.toLogString() }}" + + " footerComponents=${footerComponents.joinToString { it.toLogString() }}" + + " bottomBarComponent=${bottomBarComponent.toLogString()}" + + " )" +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt index 5f4dbfb4235e..41c80fa58527 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt @@ -32,3 +32,5 @@ data class ComponentState( val component: VolumePanelUiComponent, val isVisible: Boolean, ) + +fun ComponentState.toLogString(): String = "$key:visible=$isVisible" diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt index f495a02f6cc7..2f60c4b29a81 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt @@ -19,23 +19,30 @@ package com.android.systemui.volume.panel.ui.viewmodel import android.content.Context import android.content.IntentFilter import android.content.res.Resources +import com.android.systemui.Dumpable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dump.DumpManager import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onConfigChanged +import com.android.systemui.util.kotlin.launchAndDispose import com.android.systemui.volume.VolumePanelDialogReceiver import com.android.systemui.volume.panel.dagger.VolumePanelComponent import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.ui.composable.ComponentsFactory import com.android.systemui.volume.panel.ui.layout.ComponentsLayout import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.layout.toLogString +import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -43,19 +50,23 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +private const val TAG = "VolumePanelViewModel" + // Can't inject a constructor here because VolumePanelComponent provides this view model for its // components. +@OptIn(ExperimentalCoroutinesApi::class) class VolumePanelViewModel( resources: Resources, coroutineScope: CoroutineScope, daggerComponentFactory: VolumePanelComponentFactory, configurationController: ConfigurationController, broadcastDispatcher: BroadcastDispatcher, + private val dumpManager: DumpManager, + private val logger: VolumePanelLogger, private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor, -) { +) : Dumpable { private val volumePanelComponent: VolumePanelComponent = daggerComponentFactory.create(this, coroutineScope) @@ -77,9 +88,10 @@ class VolumePanelViewModel( .onStart { emit(resources.configuration) } .map { configuration -> VolumePanelState( - orientation = configuration.orientation, - isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen), - ) + orientation = configuration.orientation, + isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen), + ) + .also { logger.onVolumePanelStateChanged(it) } } .stateIn( scope, @@ -89,7 +101,7 @@ class VolumePanelViewModel( isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen) ), ) - val componentsLayout: Flow<ComponentsLayout> = + val componentsLayout: StateFlow<ComponentsLayout?> = combine( componentsInteractor.components, volumePanelState, @@ -104,13 +116,18 @@ class VolumePanelViewModel( } componentsLayoutManager.layout(scope, componentStates) } - .shareIn( + .stateIn( scope, SharingStarted.Eagerly, - replay = 1, + null, ) init { + scope.launchAndDispose { + dumpManager.registerNormalDumpable(TAG, this) + DisposableHandle { dumpManager.unregisterDumpable(TAG) } + } + volumePanelComponent.volumePanelStartables().onEach(VolumePanelStartable::start) broadcastDispatcher .broadcastFlow(IntentFilter(VolumePanelDialogReceiver.DISMISS_ACTION)) @@ -122,6 +139,13 @@ class VolumePanelViewModel( volumePanelGlobalStateInteractor.setVisible(false) } + override fun dump(pw: PrintWriter, args: Array<out String>) { + with(pw) { + println("volumePanelState=${volumePanelState.value}") + println("componentsLayout=${componentsLayout.value?.toLogString()}") + } + } + class Factory @Inject constructor( @@ -129,6 +153,8 @@ class VolumePanelViewModel( private val daggerComponentFactory: VolumePanelComponentFactory, private val configurationController: ConfigurationController, private val broadcastDispatcher: BroadcastDispatcher, + private val dumpManager: DumpManager, + private val logger: VolumePanelLogger, private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor, ) { @@ -139,6 +165,8 @@ class VolumePanelViewModel( daggerComponentFactory, configurationController, broadcastDispatcher, + dumpManager, + logger, volumePanelGlobalStateInteractor, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt index 869a82a78848..d6b159e9b13a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt @@ -16,7 +16,8 @@ package com.android.systemui.volume.shared -import com.android.settingslib.volume.data.repository.AudioRepositoryImpl +import com.android.settingslib.volume.shared.AudioLogger +import com.android.settingslib.volume.shared.AudioSharingLogger import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.systemui.dagger.SysUISingleton @@ -30,7 +31,7 @@ private const val TAG = "SysUI_Volume" /** Logs general System UI volume events. */ @SysUISingleton class VolumeLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) : - AudioRepositoryImpl.Logger { + AudioLogger, AudioSharingLogger { override fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) { logBuffer.log( @@ -55,4 +56,35 @@ class VolumeLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuff { "Volume update received: stream=$str1 volume=$int1" } ) } + + override fun onAudioSharingStateChanged(state: Boolean) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = state }, + { "Audio sharing state update: state=$bool1" } + ) + } + + override fun onSecondaryGroupIdChanged(groupId: Int) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { int1 = groupId }, + { "Secondary group id in audio sharing update: groupId=$int1" } + ) + } + + override fun onVolumeMapChanged(map: Map<Int, Int>) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { str1 = map.toString() }, + { "Volume map update: map=$str1" } + ) + } + + override fun onSetDeviceVolumeRequested(volume: Int) { + logBuffer.log(TAG, LogLevel.DEBUG, { int1 = volume }, { "Set device volume: volume=$int1" }) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index dc69cdadc320..f94a6f24a106 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -29,7 +29,6 @@ import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintSensorPropertiesInternal -import android.os.Handler import android.os.IBinder import android.os.UserManager import android.testing.TestableLooper @@ -45,7 +44,6 @@ import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils import com.android.launcher3.icons.IconProvider -import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository @@ -105,7 +103,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Mock lateinit var fingerprintManager: FingerprintManager @Mock lateinit var lockPatternUtils: LockPatternUtils @Mock lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock lateinit var panelInteractionDetector: AuthDialogPanelInteractionDetector @Mock lateinit var windowToken: IBinder @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var vibrator: VibratorHelper @@ -265,14 +262,6 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test - fun testActionCancel_panelInteractionDetectorDisable() { - val container = initializeFingerprintContainer() - container.mBiometricCallback.onUserCanceled() - waitForIdleSync() - verify(panelInteractionDetector).disable() - } - - @Test fun testActionAuthenticated_sendsDismissedAuthenticated() { val container = initializeFingerprintContainer() container.mBiometricCallback.onAuthenticated() @@ -416,19 +405,7 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test - fun testShowBiometricUI() { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) - val container = initializeFingerprintContainer() - - waitForIdleSync() - - assertThat(container.hasCredentialView()).isFalse() - assertThat(container.hasBiometricPrompt()).isTrue() - } - - @Test fun testShowBiometricUI_ContentViewWithMoreOptionsButton() { - mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) var isButtonClicked = false val contentView = @@ -466,7 +443,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI_withVerticalListContentView() { - mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val container = initializeFingerprintContainer( @@ -488,7 +464,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI_withContentViewWithMoreOptionsButton() { - mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView = PromptContentViewWithMoreOptionsButton.Builder() @@ -674,7 +649,6 @@ open class AuthContainerViewTest : SysuiTestCase() { fingerprintProps, faceProps, wakefulnessLifecycle, - panelInteractionDetector, userManager, lockPatternUtils, interactionJankMonitor, @@ -690,7 +664,6 @@ open class AuthContainerViewTest : SysuiTestCase() { activityTaskManager ), { credentialViewModel }, - Handler(TestableLooper.get(this).looper), fakeExecutor, vibrator ) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt deleted file mode 100644 index 023148603b50..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt +++ /dev/null @@ -1,156 +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.platform.test.flag.junit.FlagsParameterization -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.andSceneContainer -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testScope -import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.shadeTestUtil -import com.android.systemui.testKosmos -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.verifyZeroInteractions -import platform.test.runner.parameterized.ParameterizedAndroidJunit4 -import platform.test.runner.parameterized.Parameters - -@SmallTest -@RunWith(ParameterizedAndroidJunit4::class) -class AuthDialogPanelInteractionDetectorTest(flags: FlagsParameterization?) : SysuiTestCase() { - - companion object { - @JvmStatic - @Parameters(name = "{0}") - fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() - } - } - - init { - mSetFlagsRule.setFlagsParameterization(flags!!) - } - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private val shadeTestUtil by lazy { kosmos.shadeTestUtil } - - @Mock private lateinit var action: Runnable - - lateinit var detector: AuthDialogPanelInteractionDetector - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - detector = - AuthDialogPanelInteractionDetector( - kosmos.applicationCoroutineScope, - { kosmos.shadeInteractor }, - ) - } - - @Test - fun enableDetector_expand_shouldRunAction() = - testScope.runTest { - // GIVEN shade is closed and detector is enabled - shadeTestUtil.setShadeExpansion(0f) - detector.enable(action) - runCurrent() - - // WHEN shade expands - shadeTestUtil.setTracking(true) - shadeTestUtil.setShadeExpansion(.5f) - runCurrent() - - // THEN action was run - verify(action).run() - } - - @Test - fun enableDetector_isUserInteractingTrue_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN isInteracting starts true - shadeTestUtil.setTracking(true) - runCurrent() - detector.enable(action) - - // THEN action was not run - verifyZeroInteractions(action) - } - - @Test - fun enableDetector_shadeExpandImmediate_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN shade is closed and detector is enabled - shadeTestUtil.setShadeExpansion(0f) - detector.enable(action) - runCurrent() - - // WHEN shade expands fully instantly - shadeTestUtil.setShadeExpansion(1f) - runCurrent() - - // THEN action not run - verifyZeroInteractions(action) - detector.disable() - } - - @Test - fun disableDetector_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN shade is closed and detector is enabled - shadeTestUtil.setShadeExpansion(0f) - detector.enable(action) - runCurrent() - - // WHEN detector is disabled and shade opens - detector.disable() - runCurrent() - shadeTestUtil.setTracking(true) - shadeTestUtil.setShadeExpansion(.5f) - runCurrent() - - // THEN action not run - verifyZeroInteractions(action) - } - - @Test - fun enableDetector_beginCollapse_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN shade is open and detector is enabled - shadeTestUtil.setShadeExpansion(1f) - detector.enable(action) - runCurrent() - - // WHEN shade begins to collapse - shadeTestUtil.programmaticCollapseShade() - runCurrent() - - // THEN action not run - verifyZeroInteractions(action) - detector.disable() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index 720f2071ac73..dc499cd2fe8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -155,7 +155,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { Authenticators.BIOMETRIC_STRONG } isDeviceCredentialAllowed = allowCredentialFallback - componentNameForConfirmDeviceCredentialActivity = + realCallerForConfirmDeviceCredentialActivity = if (setComponentNameForConfirmDeviceCredentialActivity) componentNameOverriddenForConfirmDeviceCredentialActivity else null 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 534f25c33900..6047e7d1bf79 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 @@ -43,7 +43,6 @@ import android.view.MotionEvent import android.view.Surface import androidx.test.filters.SmallTest import com.android.app.activityTaskManager -import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.Utils.toBitmap @@ -1385,7 +1384,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionOverriddenByVerticalListContentView() = runGenericTest(description = "test description", contentView = promptContentView) { val contentView by collectLastValue(kosmos.promptViewModel.contentView) @@ -1396,7 +1395,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionOverriddenByContentViewWithMoreOptionsButton() = runGenericTest( description = "test description", @@ -1410,7 +1409,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionWithoutContentView() = runGenericTest(description = "test description") { val contentView by collectLastValue(kosmos.promptViewModel.contentView) @@ -1421,7 +1420,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_nullIfPkgNameNotFound() = runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1430,7 +1429,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_defaultFromActivityInfo() = runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1445,7 +1444,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_defaultIsNull() = runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1454,7 +1453,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_default() = runGenericTest { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) assertThat(logoInfo).isNotNull() @@ -1462,7 +1461,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_resSetByApp() = runGenericTest(logoRes = logoResFromApp) { val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap() @@ -1472,7 +1471,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_bitmapSetByApp() = runGenericTest(logoBitmap = logoBitmapFromApp) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1480,7 +1479,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_emptyIfPkgNameNotFound() = runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1488,7 +1487,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_defaultFromActivityInfo() = runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1500,7 +1499,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_defaultIsEmpty() = runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1508,14 +1507,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_default() = runGenericTest { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo) } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_setByApp() = runGenericTest(logoDescription = logoDescriptionFromApp) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1523,7 +1522,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_bottom_rotation0() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1531,7 +1529,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } // TODO(b/335278136): Add test for no sensor landscape @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_bottom_forceLarge() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) kosmos.promptViewModel.onSwitchToCredential() @@ -1540,7 +1537,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_bottom_largeScreen() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) kosmos.displayStateRepository.setIsLargeScreen(true) @@ -1549,7 +1545,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_right_rotation90() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1557,7 +1552,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_left_rotation270() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1565,7 +1559,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_top_rotation180() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1577,7 +1570,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_bottom() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds) @@ -1585,7 +1577,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } // TODO(b/335278136): Add test for no sensor landscape @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_right() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) @@ -1602,7 +1593,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_right_onlyShortTitle() = runGenericTest(subtitle = "") { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) @@ -1617,7 +1607,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_left() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) @@ -1634,7 +1623,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_left_onlyShortTitle() = runGenericTest(subtitle = "") { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) @@ -1649,7 +1637,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_top() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 3b2cf61dde68..0db7b62b8ef1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -32,7 +32,6 @@ import androidx.test.filters.SmallTest import com.airbnb.lottie.model.KeyPath import com.android.keyguard.keyguardUpdateMonitor import com.android.settingslib.Utils -import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider import com.android.systemui.biometrics.data.repository.biometricStatusRepository @@ -199,7 +198,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @Test fun updatesOverlayViewParams_onDisplayRotationChange_xAlignedSensor() { kosmos.testScope.runTest { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) setupTestConfiguration( DeviceConfig.X_ALIGNED, rotation = DisplayRotation.ROTATION_0, @@ -230,11 +228,11 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .isEqualTo( displayWidth - sensorLocation.sensorLocationX - sensorLocation.sensorRadius * 2 ) - assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight) + assertThat(overlayViewParams!!.y).isEqualTo(displayHeight) kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) assertThat(overlayViewParams).isNotNull() - assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParams!!.x).isEqualTo(displayWidth) assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationX) } } @@ -242,7 +240,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @Test fun updatesOverlayViewParams_onDisplayRotationChange_yAlignedSensor() { kosmos.testScope.runTest { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) setupTestConfiguration( DeviceConfig.Y_ALIGNED, rotation = DisplayRotation.ROTATION_0, @@ -256,7 +253,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { runCurrent() assertThat(overlayViewParams).isNotNull() - assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParams!!.x).isEqualTo(displayWidth) assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationY) kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) @@ -278,7 +275,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .isEqualTo( displayWidth - sensorLocation.sensorLocationY - sensorLocation.sensorRadius * 2 ) - assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight) + assertThat(overlayViewParams!!.y).isEqualTo(displayHeight) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index e3a38a8d6763..37f1a3d73b0c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -443,7 +443,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -463,7 +463,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -570,7 +570,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // When showing and provisioned mViewMediator.onSystemReady(); when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); // and a SIM becomes locked and requires a PIN mViewMediator.mUpdateCallback.onSimStateChanged( @@ -579,7 +579,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TelephonyManager.SIM_STATE_PIN_REQUIRED); // and the keyguard goes away - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); @@ -595,7 +595,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // When showing and provisioned mViewMediator.onSystemReady(); when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); // and a SIM becomes locked and requires a PIN mViewMediator.mUpdateCallback.onSimStateChanged( @@ -604,7 +604,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TelephonyManager.SIM_STATE_PIN_REQUIRED); // and the keyguard goes away - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); @@ -843,7 +843,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -863,7 +863,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -978,7 +978,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileInteractive_resets() { - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); when(mKeyguardStateController.isShowing()).thenReturn(true); TestableLooper.get(this).processAllMessages(); @@ -992,7 +992,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() { - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); when(mKeyguardStateController.isShowing()).thenReturn(true); TestableLooper.get(this).processAllMessages(); @@ -1051,7 +1051,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); processAllMessagesAndBgExecutorMessages(); - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{ mock(RemoteAnimationTarget.class) @@ -1123,7 +1123,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testNotStartingKeyguardWhenFlagIsDisabled() { - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.onDreamingStarted(); @@ -1133,7 +1133,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testStartingKeyguardWhenFlagIsEnabled() { - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); when(mKeyguardStateController.isShowing()).thenReturn(true); mViewMediator.onDreamingStarted(); @@ -1174,7 +1174,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); // WHEN keyguard visibility becomes FALSE - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); keyguardUpdateMonitorCallback.onKeyguardVisibilityChanged(false); TestableLooper.get(this).processAllMessages(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index 2af4d872f6a0..bfe89de6229d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -17,9 +17,6 @@ package com.android.systemui.keyguard.data.repository import android.animation.ValueAnimator -import android.util.Log -import android.util.Log.TerribleFailure -import android.util.Log.TerribleFailureHandler import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest @@ -53,7 +50,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -67,23 +63,14 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { private val testScope = kosmos.testScope private lateinit var underTest: KeyguardTransitionRepository - private lateinit var oldWtfHandler: TerribleFailureHandler - private lateinit var wtfHandler: WtfHandler private lateinit var runner: KeyguardTransitionRunner @Before fun setUp() { underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main) - wtfHandler = WtfHandler() - oldWtfHandler = Log.setWtfHandler(wtfHandler) runner = KeyguardTransitionRunner(underTest) } - @After - fun tearDown() { - oldWtfHandler?.let { Log.setWtfHandler(it) } - } - @Test fun startTransitionRunsAnimatorToCompletion() = testScope.runTest { @@ -333,15 +320,17 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } @Test - fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() = + fun attemptTomanuallyUpdateTransitionWithInvalidUUIDEmitsNothing() = testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) - assertThat(wtfHandler.failed).isTrue() + assertThat(steps.size).isEqualTo(0) } @Test - fun attemptToManuallyUpdateTransitionAfterFINISHEDstateThrowsException() = + fun attemptToManuallyUpdateTransitionAfterFINISHEDstateEmitsNothing() = testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) val uuid = underTest.startTransition( TransitionInfo( @@ -356,12 +345,19 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { underTest.updateTransition(it, 1f, TransitionState.FINISHED) underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) } - assertThat(wtfHandler.failed).isTrue() + assertThat(steps.size).isEqualTo(2) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) + assertThat(steps[1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME) + ) } @Test - fun attemptToManuallyUpdateTransitionAfterCANCELEDstateThrowsException() = + fun attemptToManuallyUpdateTransitionAfterCANCELEDstateEmitsNothing() = testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) val uuid = underTest.startTransition( TransitionInfo( @@ -376,7 +372,13 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { underTest.updateTransition(it, 0.2f, TransitionState.CANCELED) underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) } - assertThat(wtfHandler.failed).isTrue() + assertThat(steps.size).isEqualTo(2) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) + assertThat(steps[1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, 0.2f, TransitionState.CANCELED, OWNER_NAME) + ) } @Test @@ -530,8 +532,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } assertThat(steps[steps.size - 1]) .isEqualTo(TransitionStep(from, to, lastValue, status, OWNER_NAME)) - - assertThat(wtfHandler.failed).isFalse() } private fun getAnimator(): ValueAnimator { @@ -541,14 +541,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } } - private class WtfHandler : TerribleFailureHandler { - var failed = false - - override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { - failed = true - } - } - companion object { private const val OWNER_NAME = "KeyguardTransitionRunner" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index 313292a5fab8..d8e96bc23b25 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -51,14 +51,14 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { @Test fun showPrimaryBouncer() = testScope.runTest { - underTest.showPrimaryBouncer() + underTest.onTapped() verify(statusBarKeyguardViewManager).showPrimaryBouncer(any()) } @Test fun hideAlternateBouncer() = testScope.runTest { - underTest.hideAlternateBouncer() + underTest.onRemovedFromWindow() verify(statusBarKeyguardViewManager).hideAlternateBouncer(any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt index 11b0bdf3effd..7dae5ccd05c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt @@ -21,13 +21,13 @@ import android.os.UserHandle import android.testing.TestableLooper import android.view.View import android.widget.Spinner +import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Dependency import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.flags.FeatureFlags import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN @@ -60,7 +60,6 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { @Mock private lateinit var starter: ActivityStarter @Mock private lateinit var controller: RecordingController @Mock private lateinit var userContextProvider: UserContextProvider - @Mock private lateinit var flags: FeatureFlags @Mock private lateinit var onStartRecordingClicked: Runnable @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger @@ -128,6 +127,32 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { } @Test + fun startButtonText_entireScreenSelected() { + showDialog() + + onSpinnerItemSelected(ENTIRE_SCREEN) + + assertThat(getStartButton().text) + .isEqualTo( + context.getString(R.string.screenrecord_permission_dialog_continue_entire_screen) + ) + } + + @Test + fun startButtonText_singleAppSelected() { + showDialog() + + onSpinnerItemSelected(SINGLE_APP) + + assertThat(getStartButton().text) + .isEqualTo( + context.getString( + R.string.media_projection_entry_generic_permission_dialog_continue_single_app + ) + ) + } + + @Test fun startClicked_singleAppSelected_passesHostUidToAppSelector() { showDialog() onSpinnerItemSelected(SINGLE_APP) @@ -152,7 +177,8 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { showDialog() val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) - val singleApp = context.getString(R.string.screen_share_permission_dialog_option_single_app) + val singleApp = + context.getString(R.string.screenrecord_permission_dialog_option_text_single_app) assertEquals(spinner.adapter.getItem(0), singleApp) } @@ -208,8 +234,10 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { dialog.requireViewById<View>(android.R.id.button2).performClick() } + private fun getStartButton() = dialog.requireViewById<TextView>(android.R.id.button1) + private fun clickOnStart() { - dialog.requireViewById<View>(android.R.id.button1).performClick() + getStartButton().performClick() } private fun onSpinnerItemSelected(position: Int) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt index 2919d3f575c6..1e95fc12bdb5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.ui.binder import android.content.applicationContext -import android.view.layoutInflater import android.view.mockedLayoutInflater import android.view.windowManager import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor @@ -25,7 +24,6 @@ import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel -import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel @@ -50,10 +48,10 @@ val Kosmos.alternateBouncerViewBinder by alternateBouncerDependencies = { alternateBouncerDependencies }, windowManager = { windowManager }, layoutInflater = { mockedLayoutInflater }, - dismissCallbackRegistry = dismissCallbackRegistry, ) } +@ExperimentalCoroutinesApi private val Kosmos.alternateBouncerDependencies by Kosmos.Fixture { AlternateBouncerDependencies( @@ -69,6 +67,7 @@ private val Kosmos.alternateBouncerDependencies by ) } +@ExperimentalCoroutinesApi private val Kosmos.alternateBouncerUdfpsIconViewModel by Kosmos.Fixture { AlternateBouncerUdfpsIconViewModel( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt index bdd4afa3da7d..29583153ccc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -28,5 +30,7 @@ val Kosmos.alternateBouncerViewModel by Fixture { AlternateBouncerViewModel( statusBarKeyguardViewManager = statusBarKeyguardViewManager, keyguardTransitionInteractor = keyguardTransitionInteractor, + dismissCallbackRegistry = dismissCallbackRegistry, + alternateBouncerInteractor = { alternateBouncerInteractor }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt new file mode 100644 index 000000000000..73b1859acc3d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.airplane + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger +import com.android.systemui.statusbar.connectivity.ConnectivityModule + +val Kosmos.qsAirplaneModeTileConfig by + Kosmos.Fixture { ConnectivityModule.provideAirplaneModeTileConfig(qsEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt index b6194e3f511d..bbe753e8c7a5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt @@ -27,7 +27,9 @@ import kotlinx.coroutines.flow.filterNotNull class FakeQSSceneAdapter( private val inflateDelegate: suspend (Context) -> View, override val qqsHeight: Int = 0, + override val squishedQqsHeight: Int = 0, override val qsHeight: Int = 0, + override val squishedQsHeight: Int = 0, ) : QSSceneAdapter { private val _customizerState = MutableStateFlow<CustomizerState>(CustomizerState.Hidden) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 8e76a0bf5a13..53b6a2ee226b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.falsingCollector @@ -80,5 +81,6 @@ val Kosmos.sceneContainerStartable by Fixture { keyguardEnabledInteractor = keyguardEnabledInteractor, dismissCallbackRegistry = dismissCallbackRegistry, statusBarStateController = sysuiStatusBarStateController, + alternateBouncerInteractor = alternateBouncerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt index 2ba1211a9bdb..0b438d183544 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.data.repository import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.shared.volumePanelLogger val Kosmos.volumePanelGlobalStateRepository by - Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager) } + Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager, volumePanelLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt index a18f498e5441..3804a9f21080 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt @@ -28,6 +28,7 @@ import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.defaultCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.ui.composable.enabledComponents +import com.android.systemui.volume.shared.volumePanelLogger import javax.inject.Provider var Kosmos.criteriaByKey: Map<VolumePanelComponentKey, Provider<ComponentAvailabilityCriteria>> by @@ -50,6 +51,7 @@ var Kosmos.componentsInteractor: ComponentsInteractor by enabledComponents, { defaultCriteria }, testScope.backgroundScope, + volumePanelLogger, criteriaByKey, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt index 34a008f92518..c4fb9e486c4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt @@ -18,17 +18,19 @@ package com.android.systemui.volume.panel.ui.viewmodel import android.content.applicationContext import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor +import com.android.systemui.volume.shared.volumePanelLogger var Kosmos.volumePanelStartables: Set<VolumePanelStartable> by Kosmos.Fixture { emptySet() } var Kosmos.volumePanelViewModel: VolumePanelViewModel by - Kosmos.Fixture { volumePanelViewModelFactory.create(testScope.backgroundScope) } + Kosmos.Fixture { volumePanelViewModelFactory.create(applicationCoroutineScope) } val Kosmos.volumePanelViewModelFactory: VolumePanelViewModel.Factory by Kosmos.Fixture { @@ -37,6 +39,8 @@ val Kosmos.volumePanelViewModelFactory: VolumePanelViewModel.Factory by volumePanelComponentFactory, configurationController, broadcastDispatcher, + dumpManager, + volumePanelLogger, volumePanelGlobalStateInteractor, ) } diff --git a/services/Android.bp b/services/Android.bp index ded7379ad487..0006455f41b0 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -120,6 +120,7 @@ filegroup { ":services.backup-sources", ":services.companion-sources", ":services.contentcapture-sources", + ":services.appfunctions-sources", ":services.contentsuggestions-sources", ":services.contextualsearch-sources", ":services.coverage-sources", @@ -217,6 +218,7 @@ system_java_library { "services.autofill", "services.backup", "services.companion", + "services.appfunctions", "services.contentcapture", "services.contentsuggestions", "services.contextualsearch", diff --git a/services/appfunctions/Android.bp b/services/appfunctions/Android.bp new file mode 100644 index 000000000000..f8ee823ef0c9 --- /dev/null +++ b/services/appfunctions/Android.bp @@ -0,0 +1,25 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "services.appfunctions-sources", + srcs: ["java/**/*.java"], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.appfunctions", + defaults: ["platform_service_defaults"], + srcs: [ + ":services.appfunctions-sources", + "java/**/*.logtags", + ], + libs: ["services.core"], +} diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java new file mode 100644 index 000000000000..f30e770be32b --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appfunctions; + +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; + +import android.app.appfunctions.IAppFunctionManager; +import android.content.Context; + +import com.android.server.SystemService; + +/** + * Service that manages app functions. + */ +public class AppFunctionManagerService extends SystemService { + + public AppFunctionManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + if (enableAppFunctionManager()) { + publishBinderService(Context.APP_FUNCTION_SERVICE, new AppFunctionManagerStub()); + } + } + + private static class AppFunctionManagerStub extends IAppFunctionManager.Stub { + + } +} diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 2e1416b2887b..d4f729cfbaf6 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -6962,7 +6962,8 @@ public final class ActiveServices { } private boolean collectPackageServicesLocked(String packageName, Set<String> filterByClasses, - boolean evenPersistent, boolean doit, ArrayMap<ComponentName, ServiceRecord> services) { + boolean evenPersistent, boolean doit, int minOomAdj, + ArrayMap<ComponentName, ServiceRecord> services) { boolean didSomething = false; for (int i = services.size() - 1; i >= 0; i--) { ServiceRecord service = services.valueAt(i); @@ -6970,6 +6971,11 @@ public final class ActiveServices { || (service.packageName.equals(packageName) && (filterByClasses == null || filterByClasses.contains(service.name.getClassName()))); + if (service.app != null && service.app.mState.getCurAdj() < minOomAdj) { + Slog.i(TAG, "Skip force stopping service " + service + + ": below minimum oom adj level"); + continue; + } if (sameComponent && (service.app == null || evenPersistent || !service.app.isPersistent())) { if (!doit) { @@ -6993,6 +6999,12 @@ public final class ActiveServices { boolean bringDownDisabledPackageServicesLocked(String packageName, Set<String> filterByClasses, int userId, boolean evenPersistent, boolean fullStop, boolean doit) { + return bringDownDisabledPackageServicesLocked(packageName, filterByClasses, userId, + evenPersistent, fullStop, doit, ProcessList.INVALID_ADJ); + } + + boolean bringDownDisabledPackageServicesLocked(String packageName, Set<String> filterByClasses, + int userId, boolean evenPersistent, boolean fullStop, boolean doit, int minOomAdj) { boolean didSomething = false; if (mTmpCollectionResults != null) { @@ -7002,7 +7014,8 @@ public final class ActiveServices { if (userId == UserHandle.USER_ALL) { for (int i = mServiceMap.size() - 1; i >= 0; i--) { didSomething |= collectPackageServicesLocked(packageName, filterByClasses, - evenPersistent, doit, mServiceMap.valueAt(i).mServicesByInstanceName); + evenPersistent, doit, minOomAdj, + mServiceMap.valueAt(i).mServicesByInstanceName); if (!doit && didSomething) { return true; } @@ -7015,7 +7028,7 @@ public final class ActiveServices { if (smap != null) { ArrayMap<ComponentName, ServiceRecord> items = smap.mServicesByInstanceName; didSomething = collectPackageServicesLocked(packageName, filterByClasses, - evenPersistent, doit, items); + evenPersistent, doit, minOomAdj, items); } if (doit && filterByClasses == null) { forceStopPackageLocked(packageName, userId); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 9d8f3374d6ad..4a18cb1f5ed8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4377,6 +4377,16 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") + final boolean forceStopUserPackagesLocked(int userId, String reasonString, + boolean evenImportantServices) { + int minOomAdj = evenImportantServices ? ProcessList.INVALID_ADJ + : ProcessList.FOREGROUND_APP_ADJ; + return forceStopPackageInternalLocked(null, -1, false, false, + true, false, false, false, userId, reasonString, + ApplicationExitInfo.REASON_USER_STOPPED, minOomAdj); + } + + @GuardedBy("this") final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, @@ -4385,7 +4395,6 @@ public class ActivityManagerService extends IActivityManager.Stub : ApplicationExitInfo.REASON_USER_REQUESTED; return forceStopPackageLocked(packageName, appId, callerWillRestart, purgeCache, doit, evenPersistent, uninstalling, packageStateStopped, userId, reasonString, reason); - } @GuardedBy("this") @@ -4393,6 +4402,16 @@ public class ActivityManagerService extends IActivityManager.Stub boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, int userId, String reasonString, int reason) { + return forceStopPackageInternalLocked(packageName, appId, callerWillRestart, purgeCache, + doit, evenPersistent, uninstalling, packageStateStopped, userId, reasonString, + reason, ProcessList.INVALID_ADJ); + } + + @GuardedBy("this") + private boolean forceStopPackageInternalLocked(String packageName, int appId, + boolean callerWillRestart, boolean purgeCache, boolean doit, + boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, + int userId, String reasonString, int reason, int minOomAdj) { int i; if (userId == UserHandle.USER_ALL && packageName == null) { @@ -4431,7 +4450,7 @@ public class ActivityManagerService extends IActivityManager.Stub } didSomething |= mProcessList.killPackageProcessesLSP(packageName, appId, userId, - ProcessList.INVALID_ADJ, callerWillRestart, false /* allowRestart */, doit, + minOomAdj, callerWillRestart, false /* allowRestart */, doit, evenPersistent, true /* setRemoved */, uninstalling, reason, subReason, @@ -4440,7 +4459,8 @@ public class ActivityManagerService extends IActivityManager.Stub } if (mServices.bringDownDisabledPackageServicesLocked( - packageName, null /* filterByClasses */, userId, evenPersistent, true, doit)) { + packageName, null /* filterByClasses */, userId, evenPersistent, + true, doit, minOomAdj)) { if (!doit) { return true; } @@ -19872,6 +19892,11 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId) { + return mUserController.isEarlyPackageKillEnabledForUserSwitch(fromUserId, toUserId); + } + + @Override public void setStopUserOnSwitch(int value) { ActivityManagerService.this.setStopUserOnSwitch(value); } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 30efa3e87fc6..e57fe133eac8 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -439,6 +439,15 @@ class UserController implements Handler.Callback { @GuardedBy("mLock") private final List<PendingUserStart> mPendingUserStarts = new ArrayList<>(); + /** + * Contains users which cannot abort the shutdown process. + * + * <p> For example, we don't abort shutdown for users whose processes have already been stopped + * due to {@link #isEarlyPackageKillEnabledForUserSwitch(int, int)}. + */ + @GuardedBy("mLock") + private final ArraySet<Integer> mDoNotAbortShutdownUserIds = new ArraySet<>(); + private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() { @Override public void onUserCreated(UserInfo user, Object token) { @@ -509,11 +518,11 @@ class UserController implements Handler.Callback { } } - private boolean shouldStopUserOnSwitch() { + private boolean isStopUserOnSwitchEnabled() { synchronized (mLock) { if (mStopUserOnSwitch != STOP_USER_ON_SWITCH_DEFAULT) { final boolean value = mStopUserOnSwitch == STOP_USER_ON_SWITCH_TRUE; - Slogf.i(TAG, "shouldStopUserOnSwitch(): returning overridden value (%b)", value); + Slogf.i(TAG, "isStopUserOnSwitchEnabled(): returning overridden value (%b)", value); return value; } } @@ -521,6 +530,26 @@ class UserController implements Handler.Callback { return property == -1 ? mDelayUserDataLocking : property == 1; } + /** + * Get whether or not the previous user's packages will be killed before the user is + * stopped during a user switch. + * + * <p> The primary use case of this method is for {@link com.android.server.SystemService} + * classes to call this API in their + * {@link com.android.server.SystemService#onUserSwitching} method implementation to prevent + * restarting any of the previous user's processes that will be killed during the user switch. + */ + boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId) { + // NOTE: The logic in this method could be extended to cover other cases where + // the previous user is also stopped like: guest users, ephemeral users, + // and users with DISALLOW_RUN_IN_BACKGROUND. Currently, this is not done + // because early killing is not enabled for these cases by default. + if (fromUserId == UserHandle.USER_SYSTEM) { + return false; + } + return isStopUserOnSwitchEnabled(); + } + void finishUserSwitch(UserState uss) { // This call holds the AM lock so we post to the handler. mHandler.post(() -> { @@ -1247,6 +1276,7 @@ class UserController implements Handler.Callback { return; } uss.setState(UserState.STATE_SHUTDOWN); + mDoNotAbortShutdownUserIds.remove(userId); } TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("setUserState-STATE_SHUTDOWN-" + userId + "-[stopUser]"); @@ -1555,7 +1585,8 @@ class UserController implements Handler.Callback { private void stopPackagesOfStoppedUser(@UserIdInt int userId, String reason) { if (DEBUG_MU) Slogf.i(TAG, "stopPackagesOfStoppedUser(%d): %s", userId, reason); - mInjector.activityManagerForceStopPackage(userId, reason); + mInjector.activityManagerForceStopUserPackages(userId, reason, + /* evenImportantServices= */ true); if (mInjector.getUserManager().isPreCreated(userId)) { // Don't fire intent for precreated. return; @@ -1608,6 +1639,21 @@ class UserController implements Handler.Callback { } } + private void stopPreviousUserPackagesIfEnabled(int fromUserId, int toUserId) { + if (!android.multiuser.Flags.stopPreviousUserApps() + || !isEarlyPackageKillEnabledForUserSwitch(fromUserId, toUserId)) { + return; + } + // Stop the previous user's packages early to reduce resource usage + // during user switching. Only do this when the previous user will + // be stopped regardless. + synchronized (mLock) { + mDoNotAbortShutdownUserIds.add(fromUserId); + } + mInjector.activityManagerForceStopUserPackages(fromUserId, + "early stop user packages", /* evenImportantServices= */ false); + } + void scheduleStartProfiles() { // Parent user transition to RUNNING_UNLOCKING happens on FgThread, so it is busy, there is // a chance the profile will reach RUNNING_LOCKED while parent is still locked, so no @@ -1889,7 +1935,8 @@ class UserController implements Handler.Callback { updateStartedUserArrayLU(); needStart = true; updateUmState = true; - } else if (uss.state == UserState.STATE_SHUTDOWN) { + } else if (uss.state == UserState.STATE_SHUTDOWN + || mDoNotAbortShutdownUserIds.contains(userId)) { Slogf.i(TAG, "User #" + userId + " is shutting down - will start after full shutdown"); mPendingUserStarts.add(new PendingUserStart(userId, userStartMode, @@ -2293,7 +2340,7 @@ class UserController implements Handler.Callback { hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, oldUserId); synchronized (mLock) { // If running in background is disabled or mStopUserOnSwitch mode, stop the user. - if (hasRestriction || shouldStopUserOnSwitch()) { + if (hasRestriction || isStopUserOnSwitchEnabled()) { Slogf.i(TAG, "Stopping user %d and its profiles on user switch", oldUserId); stopUsersLU(oldUserId, /* allowDelayedLocking= */ false, null, null); return; @@ -3425,7 +3472,7 @@ class UserController implements Handler.Callback { pw.println(" mLastActiveUsersForDelayedLocking:" + mLastActiveUsersForDelayedLocking); pw.println(" mDelayUserDataLocking:" + mDelayUserDataLocking); pw.println(" mAllowUserUnlocking:" + mAllowUserUnlocking); - pw.println(" shouldStopUserOnSwitch():" + shouldStopUserOnSwitch()); + pw.println(" isStopUserOnSwitchEnabled():" + isStopUserOnSwitchEnabled()); pw.println(" mStopUserOnSwitch:" + mStopUserOnSwitch); pw.println(" mMaxRunningUsers:" + mMaxRunningUsers); pw.println(" mBackgroundUserScheduledStopTimeSecs:" @@ -3522,6 +3569,7 @@ class UserController implements Handler.Callback { Integer.toString(msg.arg1), msg.arg1); mInjector.getSystemServiceManager().onUserSwitching(msg.arg2, msg.arg1); + stopPreviousUserPackagesIfEnabled(msg.arg2, msg.arg1); scheduleOnUserCompletedEvent(msg.arg1, UserCompletedEventType.EVENT_TYPE_USER_SWITCHING, USER_COMPLETED_EVENT_DELAY_MS); @@ -3896,10 +3944,10 @@ class UserController implements Handler.Callback { }.sendNext(); } - void activityManagerForceStopPackage(@UserIdInt int userId, String reason) { + void activityManagerForceStopUserPackages(@UserIdInt int userId, String reason, + boolean evenImportantServices) { synchronized (mService) { - mService.forceStopPackageLocked(null, -1, false, false, true, false, false, false, - userId, reason); + mService.forceStopUserPackagesLocked(userId, reason, evenImportantServices); } }; diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java index 539dbca30d6b..2ce4623a19b4 100644 --- a/services/core/java/com/android/server/appop/DiscreteRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java @@ -1413,11 +1413,11 @@ final class DiscreteRegistry { pw.print("-"); pw.print(flagsToString(mOpFlag)); pw.print("] at "); - date.setTime(discretizeTimeStamp(mNoteTime)); + date.setTime(mNoteTime); pw.print(sdf.format(date)); if (mNoteDuration != -1) { pw.print(" for "); - pw.print(discretizeDuration(mNoteDuration)); + pw.print(mNoteDuration); pw.print(" milliseconds "); } if (mAttributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE) { diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index df29ca45930e..fb8a81be4b89 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -586,6 +586,8 @@ public class Utils { } } + // LINT.IfChange + /** * Checks if a client package is running in the background. * @@ -618,4 +620,6 @@ public class Utils { return true; } + // LINT.ThenChange(frameworks/base/packages/SystemUI/shared/biometrics/src/com/android + // /systemui/biometrics/Utils.kt) } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index a44e55344fe3..66e61c076030 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -16,9 +16,6 @@ package com.android.server.notification; -import static android.service.notification.Condition.STATE_TRUE; -import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; - import android.app.INotificationManager; import android.app.NotificationManager; import android.content.ComponentName; @@ -322,20 +319,7 @@ public class ConditionProviders extends ManagedServices { final Condition c = conditions[i]; final ConditionRecord r = getRecordLocked(c.id, info.component, true /*create*/); r.info = info; - if (android.app.Flags.modesUi()) { - // if user turned on the mode, ignore the update unless the app also wants the - // mode on. this will update the origin of the mode and let the owner turn it - // off when the context ends - if (r.condition != null && r.condition.source == ORIGIN_USER_IN_SYSTEMUI) { - if (r.condition.state == STATE_TRUE && c.state == STATE_TRUE) { - r.condition = c; - } - } else { - r.condition = c; - } - } else { - r.condition = c; - } + r.condition = c; } } final int N = conditions.length; diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index 268d835e704c..d495ef5ce108 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -82,7 +82,7 @@ public class ZenModeConditions implements ConditionProviders.Callback { for (ZenRule automaticRule : config.automaticRules.values()) { if (automaticRule.component != null) { evaluateRule(automaticRule, current, trigger, processSubscriptions, false); - updateSnoozing(automaticRule); + automaticRule.reconsiderConditionOverride(); } } @@ -187,13 +187,4 @@ public class ZenModeConditions implements ConditionProviders.Callback { + rule.conditionId); } } - - private boolean updateSnoozing(ZenRule rule) { - if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) { - rule.snoozing = false; - if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId); - return true; - } - return false; - } } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 8c280edf03c0..db48835a9b82 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -37,6 +37,8 @@ import static android.service.notification.ZenModeConfig.ORIGIN_SYSTEM; import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.Preconditions.checkArgument; @@ -643,10 +645,10 @@ public class ZenModeHelper { if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) { rule.zenMode = zenMode; } - rule.snoozing = false; rule.condition = new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE); + rule.resetConditionOverride(); setConfigLocked(newConfig, /* triggeringComponent= */ null, ORIGIN_APP, "applyGlobalZenModeAsImplicitZenRule", callingUid); @@ -867,8 +869,8 @@ public class ZenModeHelper { ZenRule deletedRule = ruleToRemove.copy(); deletedRule.deletionInstant = Instant.now(mClock); // If the rule is restored it shouldn't be active (or snoozed). - deletedRule.snoozing = false; deletedRule.condition = null; + deletedRule.resetConditionOverride(); // Overwrites a previously-deleted rule with the same conditionId, but that's okay. config.deletedRules.put(deletedKey, deletedRule); } @@ -885,7 +887,12 @@ public class ZenModeHelper { if (rule == null || !canManageAutomaticZenRule(rule)) { return Condition.STATE_UNKNOWN; } - return rule.condition != null ? rule.condition.state : STATE_FALSE; + if (Flags.modesApi() && Flags.modesUi()) { + return rule.isAutomaticActive() ? STATE_TRUE : STATE_FALSE; + } else { + // Buggy, does not consider snoozing! + return rule.condition != null ? rule.condition.state : STATE_FALSE; + } } } @@ -943,12 +950,40 @@ public class ZenModeHelper { } for (ZenRule rule : rules) { - rule.condition = condition; - updateSnoozing(rule); + applyConditionAndReconsiderOverride(rule, condition, origin); setConfigLocked(config, rule.component, origin, "conditionChanged", callingUid); } } + private static void applyConditionAndReconsiderOverride(ZenRule rule, Condition condition, + int origin) { + if (Flags.modesApi() && Flags.modesUi()) { + if (origin == ORIGIN_USER_IN_SYSTEMUI && condition != null + && condition.source == SOURCE_USER_ACTION) { + // Apply as override, instead of actual condition. + if (condition.state == STATE_TRUE) { + // Manually turn on a rule -> Apply override. + rule.setConditionOverride(OVERRIDE_ACTIVATE); + } else if (condition.state == STATE_FALSE) { + // Manually turn off a rule. If the rule was manually activated before, reset + // override -- but only if this will not result in the rule turning on + // immediately because of a previously snoozed condition! In that case, apply + // deactivate-override. + rule.resetConditionOverride(); + if (rule.isAutomaticActive()) { + rule.setConditionOverride(OVERRIDE_DEACTIVATE); + } + } + } else { + rule.condition = condition; + rule.reconsiderConditionOverride(); + } + } else { + rule.condition = condition; + rule.reconsiderConditionOverride(); + } + } + private static List<ZenRule> findMatchingRules(ZenModeConfig config, Uri id, Condition condition) { List<ZenRule> matchingRules = new ArrayList<>(); @@ -971,15 +1006,6 @@ public class ZenModeHelper { return true; } - private boolean updateSnoozing(ZenRule rule) { - if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) { - rule.snoozing = false; - if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId); - return true; - } - return false; - } - public int getCurrentInstanceCount(ComponentName cn) { if (cn == null) { return 0; @@ -1181,7 +1207,7 @@ public class ZenModeHelper { if (rule.enabled != azr.isEnabled()) { rule.enabled = azr.isEnabled(); - rule.snoozing = false; + rule.resetConditionOverride(); modified = true; } if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) { @@ -1271,7 +1297,7 @@ public class ZenModeHelper { return modified; } else { if (rule.enabled != azr.isEnabled()) { - rule.snoozing = false; + rule.resetConditionOverride(); } rule.name = azr.getName(); rule.condition = null; @@ -1573,18 +1599,16 @@ public class ZenModeHelper { // For API calls (different origin) keep old behavior of snoozing all rules. for (ZenRule automaticRule : newConfig.automaticRules.values()) { if (automaticRule.isAutomaticActive()) { - automaticRule.snoozing = true; + automaticRule.setConditionOverride(OVERRIDE_DEACTIVATE); } } } } else { if (zenMode == Global.ZEN_MODE_OFF) { newConfig.manualRule = null; - // User deactivation of DND means just turning off the manual DND rule. - // For API calls (different origin) keep old behavior of snoozing all rules. for (ZenRule automaticRule : newConfig.automaticRules.values()) { if (automaticRule.isAutomaticActive()) { - automaticRule.snoozing = true; + automaticRule.setConditionOverride(OVERRIDE_DEACTIVATE); } } @@ -1626,13 +1650,11 @@ public class ZenModeHelper { void dump(ProtoOutputStream proto) { proto.write(ZenModeProto.ZEN_MODE, mZenMode); synchronized (mConfigLock) { - if (mConfig.manualRule != null) { + if (mConfig.isManualActive()) { mConfig.manualRule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS); } for (ZenRule rule : mConfig.automaticRules.values()) { - if (rule.enabled && rule.condition != null - && rule.condition.state == STATE_TRUE - && !rule.snoozing) { + if (rule.isAutomaticActive()) { rule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS); } } @@ -1695,8 +1717,8 @@ public class ZenModeHelper { for (ZenRule automaticRule : config.automaticRules.values()) { if (forRestore) { // don't restore transient state from restored automatic rules - automaticRule.snoozing = false; automaticRule.condition = null; + automaticRule.resetConditionOverride(); automaticRule.creationTime = time; } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 98e3e24c36b9..22b4d5def8f4 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -686,6 +686,9 @@ final class InstallPackageHelper { (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; final boolean fullApp = (installFlags & PackageManager.INSTALL_FULL_APP) != 0; + final boolean isPackageDeviceAdmin = mPm.isPackageDeviceAdmin(packageName, userId); + final boolean isProtectedPackage = mPm.mProtectedPackages != null + && mPm.mProtectedPackages.isPackageStateProtected(userId, packageName); // writer synchronized (mPm.mLock) { @@ -694,7 +697,8 @@ final class InstallPackageHelper { if (pkgSetting == null || pkgSetting.getPkg() == null) { return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender); } - if (instantApp && (pkgSetting.isSystem() || pkgSetting.isUpdatedSystemApp())) { + if (instantApp && (pkgSetting.isSystem() || pkgSetting.isUpdatedSystemApp() + || isPackageDeviceAdmin || isProtectedPackage)) { return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender); } if (!snapshot.canViewInstantApps(callingUid, UserHandle.getUserId(callingUid))) { diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index c40608d12a0e..c95d88e8c697 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -163,6 +163,22 @@ }, { "name": "CtsUpdateOwnershipEnforcementTestCases" + }, + { + "name": "CtsPackageInstallerCUJTestCases", + "file_patterns": [ + "core/java/.*Install.*", + "services/core/.*Install.*", + "services/core/java/com/android/server/pm/.*" + ], + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + } + ] } ], "imports": [ diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java index 46bd7af159da..fe0cf5909970 100644 --- a/services/core/java/com/android/server/vibrator/HalVibration.java +++ b/services/core/java/com/android/server/vibrator/HalVibration.java @@ -170,9 +170,11 @@ final class HalVibration extends Vibration { /** Return {@link VibrationStats.StatsInfo} with read-only metrics about this vibration. */ public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) { - int vibrationType = isRepeating() - ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED - : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE; + int vibrationType = mEffectToPlay.hasVendorEffects() + ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__VENDOR + : isRepeating() + ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED + : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE; return new VibrationStats.StatsInfo( callerInfo.uid, vibrationType, callerInfo.attrs.getUsage(), mStatus, stats, completionUptimeMillis); diff --git a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java index 8f36118543ed..407f3d996798 100644 --- a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java @@ -52,6 +52,7 @@ final class PerformVendorEffectVibratorStep extends AbstractVibratorStep { long vibratorOnResult = controller.on(effect, getVibration().id); vibratorOnResult = Math.min(vibratorOnResult, VENDOR_EFFECT_MAX_DURATION_MS); handleVibratorOnResult(vibratorOnResult); + getVibration().stats.reportPerformVendorEffect(vibratorOnResult); return List.of(new CompleteEffectVibratorStep(conductor, startTime, /* cancelled= */ false, controller, mPendingVibratorOffDeadline)); } finally { diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java index f2f5eda7c05a..0d6778c18759 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSettings.java +++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java @@ -16,7 +16,6 @@ package com.android.server.vibrator; -import static android.os.VibrationAttributes.CATEGORY_KEYBOARD; import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY; import static android.os.VibrationAttributes.USAGE_ALARM; import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST; @@ -191,8 +190,6 @@ final class VibrationSettings { @GuardedBy("mLock") private boolean mVibrateOn; @GuardedBy("mLock") - private boolean mKeyboardVibrationOn; - @GuardedBy("mLock") private int mRingerMode; @GuardedBy("mLock") private boolean mOnWirelessCharger; @@ -532,14 +529,6 @@ final class VibrationSettings { return false; } - if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) { - int category = callerInfo.attrs.getCategory(); - if (usage == USAGE_TOUCH && category == CATEGORY_KEYBOARD) { - // Keyboard touch has a different user setting. - return mKeyboardVibrationOn; - } - } - // Apply individual user setting based on usage. return getCurrentIntensity(usage) != Vibrator.VIBRATION_INTENSITY_OFF; } @@ -556,10 +545,11 @@ final class VibrationSettings { mVibrateInputDevices = loadSystemSetting(Settings.System.VIBRATE_INPUT_DEVICES, 0, userHandle) > 0; mVibrateOn = loadSystemSetting(Settings.System.VIBRATE_ON, 1, userHandle) > 0; - mKeyboardVibrationOn = loadSystemSetting( - Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0; - int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK); + boolean isKeyboardVibrationOn = loadSystemSetting( + Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0; + int keyboardIntensity = toIntensity(isKeyboardVibrationOn, + getDefaultIntensity(USAGE_IME_FEEDBACK)); int alarmIntensity = toIntensity( loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle), getDefaultIntensity(USAGE_ALARM)); @@ -654,7 +644,6 @@ final class VibrationSettings { return "VibrationSettings{" + "mVibratorConfig=" + mVibrationConfig + ", mVibrateOn=" + mVibrateOn - + ", mKeyboardVibrationOn=" + mKeyboardVibrationOn + ", mVibrateInputDevices=" + mVibrateInputDevices + ", mBatterySaverMode=" + mBatterySaverMode + ", mRingerMode=" + ringerModeToString(mRingerMode) @@ -671,7 +660,6 @@ final class VibrationSettings { pw.println("VibrationSettings:"); pw.increaseIndent(); pw.println("vibrateOn = " + mVibrateOn); - pw.println("keyboardVibrationOn = " + mKeyboardVibrationOn); pw.println("vibrateInputDevices = " + mVibrateInputDevices); pw.println("batterySaverMode = " + mBatterySaverMode); pw.println("ringerMode = " + ringerModeToString(mRingerMode)); @@ -698,8 +686,6 @@ final class VibrationSettings { void dump(ProtoOutputStream proto) { synchronized (mLock) { proto.write(VibratorManagerServiceDumpProto.VIBRATE_ON, mVibrateOn); - proto.write(VibratorManagerServiceDumpProto.KEYBOARD_VIBRATION_ON, - mKeyboardVibrationOn); proto.write(VibratorManagerServiceDumpProto.LOW_POWER_MODE, mBatterySaverMode); proto.write(VibratorManagerServiceDumpProto.ALARM_INTENSITY, getCurrentIntensity(USAGE_ALARM)); @@ -774,6 +760,11 @@ final class VibrationSettings { return value; } + @VibrationIntensity + private int toIntensity(boolean enabled, @VibrationIntensity int defaultValue) { + return enabled ? defaultValue : Vibrator.VIBRATION_INTENSITY_OFF; + } + private boolean loadBooleanSetting(String settingKey, int userHandle) { return loadSystemSetting(settingKey, 0, userHandle) != 0; } diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java index dd66809e7ae6..8179d6aea9ca 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStats.java +++ b/services/core/java/com/android/server/vibrator/VibrationStats.java @@ -79,6 +79,7 @@ final class VibrationStats { private int mVibratorSetAmplitudeCount; private int mVibratorSetExternalControlCount; private int mVibratorPerformCount; + private int mVibratorPerformVendorCount; private int mVibratorComposeCount; private int mVibratorComposePwleCount; @@ -239,6 +240,11 @@ final class VibrationStats { } } + /** Report a call to vibrator method to trigger a vendor vibration effect. */ + void reportPerformVendorEffect(long halResult) { + mVibratorPerformVendorCount++; + } + /** Report a call to vibrator method to trigger a vibration as a composition of primitives. */ void reportComposePrimitives(long halResult, PrimitiveSegment[] primitives) { mVibratorComposeCount++; @@ -313,6 +319,7 @@ final class VibrationStats { public final int halOnCount; public final int halOffCount; public final int halPerformCount; + public final int halPerformVendorCount; public final int halSetAmplitudeCount; public final int halSetExternalControlCount; public final int halCompositionSize; @@ -357,6 +364,7 @@ final class VibrationStats { halOnCount = stats.mVibratorOnCount; halOffCount = stats.mVibratorOffCount; halPerformCount = stats.mVibratorPerformCount; + halPerformVendorCount = stats.mVibratorPerformVendorCount; halSetAmplitudeCount = stats.mVibratorSetAmplitudeCount; halSetExternalControlCount = stats.mVibratorSetExternalControlCount; halCompositionSize = stats.mVibrationCompositionTotalSize; @@ -390,7 +398,8 @@ final class VibrationStats { halOnCount, halOffCount, halPerformCount, halSetAmplitudeCount, halSetExternalControlCount, halSupportedCompositionPrimitivesUsed, halSupportedEffectsUsed, halUnsupportedCompositionPrimitivesUsed, - halUnsupportedEffectsUsed, halCompositionSize, halPwleSize, adaptiveScale); + halUnsupportedEffectsUsed, halCompositionSize, halPwleSize, adaptiveScale, + halPerformVendorCount); } private static int[] filteredKeys(SparseBooleanArray supportArray, boolean supported) { diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index d38edfc39a8a..42900512de5d 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -40,6 +40,8 @@ class AppCompatController { private final AppCompatOverrides mAppCompatOverrides; @NonNull private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery; + @NonNull + private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -57,6 +59,7 @@ class AppCompatController { mTransparentPolicy, mAppCompatOverrides); mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord, wmService.mAppCompatConfiguration); + mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(mActivityRecord); } @NonNull @@ -113,6 +116,11 @@ class AppCompatController { } @NonNull + AppCompatLetterboxPolicy getAppCompatLetterboxPolicy() { + return mAppCompatLetterboxPolicy; + } + + @NonNull AppCompatFocusOverrides getAppCompatFocusOverrides() { return mAppCompatOverrides.getAppCompatFocusOverrides(); } @@ -127,4 +135,9 @@ class AppCompatController { return mAppCompatDeviceStateQuery; } + @NonNull + AppCompatLetterboxOverrides getAppCompatLetterboxOverrides() { + return mAppCompatOverrides.getAppCompatLetterboxOverrides(); + } + } diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java b/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java new file mode 100644 index 000000000000..24ed14c5398f --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.graphics.Color; +import android.util.Slog; +import android.view.WindowManager; + +import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; + +/** + * Encapsulates overrides and configuration related to the Letterboxing policy. + */ +class AppCompatLetterboxOverrides { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatLetterboxOverrides" : TAG_ATM; + + @NonNull + private final ActivityRecord mActivityRecord; + @NonNull + private final AppCompatConfiguration mAppCompatConfiguration; + + private boolean mShowWallpaperForLetterboxBackground; + + AppCompatLetterboxOverrides(@NonNull ActivityRecord activityRecord, + @NonNull AppCompatConfiguration appCompatConfiguration) { + mActivityRecord = activityRecord; + mAppCompatConfiguration = appCompatConfiguration; + } + + boolean shouldLetterboxHaveRoundedCorners() { + // TODO(b/214030873): remove once background is drawn for transparent activities + // Letterbox shouldn't have rounded corners if the activity is transparent + return mAppCompatConfiguration.isLetterboxActivityCornersRounded() + && mActivityRecord.fillsParent(); + } + + boolean isLetterboxEducationEnabled() { + return mAppCompatConfiguration.getIsEducationEnabled(); + } + + boolean hasWallpaperBackgroundForLetterbox() { + return mShowWallpaperForLetterboxBackground; + } + + boolean checkWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown) { + if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) { + mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown; + return true; + } + return false; + } + + @NonNull + Color getLetterboxBackgroundColor() { + final WindowState w = mActivityRecord.findMainWindow(); + if (w == null || w.isLetterboxedForDisplayCutout()) { + return Color.valueOf(Color.BLACK); + } + final @LetterboxBackgroundType int letterboxBackgroundType = + mAppCompatConfiguration.getLetterboxBackgroundType(); + final ActivityManager.TaskDescription taskDescription = mActivityRecord.taskDescription; + switch (letterboxBackgroundType) { + case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: + if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) { + return Color.valueOf(taskDescription.getBackgroundColorFloating()); + } + break; + case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: + if (taskDescription != null && taskDescription.getBackgroundColor() != 0) { + return Color.valueOf(taskDescription.getBackgroundColor()); + } + break; + case LETTERBOX_BACKGROUND_WALLPAPER: + if (hasWallpaperBackgroundForLetterbox()) { + // Color is used for translucent scrim that dims wallpaper. + return mAppCompatConfiguration.getLetterboxBackgroundColor(); + } + Slog.w(TAG, "Wallpaper option is selected for letterbox background but " + + "blur is not supported by a device or not supported in the current " + + "window configuration or both alpha scrim and blur radius aren't " + + "provided so using solid color background"); + break; + case LETTERBOX_BACKGROUND_SOLID_COLOR: + return mAppCompatConfiguration.getLetterboxBackgroundColor(); + default: + throw new AssertionError( + "Unexpected letterbox background type: " + letterboxBackgroundType); + } + // If picked option configured incorrectly or not supported then default to a solid color + // background. + return mAppCompatConfiguration.getLetterboxBackgroundColor(); + } + + int getLetterboxActivityCornersRadius() { + return mAppCompatConfiguration.getLetterboxActivityCornersRadius(); + } + + boolean isLetterboxActivityCornersRounded() { + return mAppCompatConfiguration.isLetterboxActivityCornersRounded(); + } + + @LetterboxBackgroundType + int getLetterboxBackgroundType() { + return mAppCompatConfiguration.getLetterboxBackgroundType(); + } + + int getLetterboxWallpaperBlurRadiusPx() { + int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx(); + return Math.max(blurRadius, 0); + } + + float getLetterboxWallpaperDarkScrimAlpha() { + float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); + // No scrim by default. + return (alpha < 0 || alpha >= 1) ? 0.0f : alpha; + } + + boolean isLetterboxWallpaperBlurSupported() { + return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class) + .isCrossWindowBlurEnabled(); + } + +} diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java new file mode 100644 index 000000000000..48a9311c0374 --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; + +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.RoundedCorner; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.statusbar.LetterboxDetails; +import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; + +/** + * Encapsulates the logic for the Letterboxing policy. + */ +class AppCompatLetterboxPolicy { + + @NonNull + private final ActivityRecord mActivityRecord; + @NonNull + private final LetterboxPolicyState mLetterboxPolicyState; + + private boolean mLastShouldShowLetterboxUi; + + AppCompatLetterboxPolicy(@NonNull ActivityRecord activityRecord) { + mActivityRecord = activityRecord; + mLetterboxPolicyState = new LetterboxPolicyState(); + } + + /** Cleans up {@link Letterbox} if it exists.*/ + void destroy() { + mLetterboxPolicyState.destroy(); + } + + /** @return {@value true} if the letterbox policy is running and the activity letterboxed. */ + boolean isRunning() { + return mLetterboxPolicyState.isRunning(); + } + + void onMovedToDisplay(int displayId) { + mLetterboxPolicyState.onMovedToDisplay(displayId); + } + + /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ + @NonNull + Rect getLetterboxInsets() { + return mLetterboxPolicyState.getLetterboxInsets(); + } + + /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */ + void getLetterboxInnerBounds(@NonNull Rect outBounds) { + mLetterboxPolicyState.getLetterboxInnerBounds(outBounds); + } + + @Nullable + LetterboxDetails getLetterboxDetails() { + return mLetterboxPolicyState.getLetterboxDetails(); + } + + /** + * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent + * when the current activity is displayed. + */ + boolean isFullyTransparentBarAllowed(@NonNull Rect rect) { + return mLetterboxPolicyState.isFullyTransparentBarAllowed(rect); + } + + void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint, + @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction inputT) { + mLetterboxPolicyState.updateLetterboxSurfaceIfNeeded(winHint, t, inputT); + } + + void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint) { + mLetterboxPolicyState.updateLetterboxSurfaceIfNeeded(winHint, + mActivityRecord.getSyncTransaction(), mActivityRecord.getPendingTransaction()); + } + + void start(@NonNull WindowState w) { + if (shouldNotLayoutLetterbox(w)) { + return; + } + updateRoundedCornersIfNeeded(w); + updateWallpaperForLetterbox(w); + if (shouldShowLetterboxUi(w)) { + mLetterboxPolicyState.layoutLetterboxIfNeeded(w); + } else { + mLetterboxPolicyState.hide(); + } + } + + @VisibleForTesting + boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) { + if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + .getIsRelaunchingAfterRequestedOrientationChanged()) { + return mLastShouldShowLetterboxUi; + } + + final boolean shouldShowLetterboxUi = + (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible() + || mActivityRecord.isVisibleRequested()) + && mainWindow.areAppWindowBoundsLetterboxed() + // Check for FLAG_SHOW_WALLPAPER explicitly instead of using + // WindowContainer#showWallpaper because the later will return true when + // this activity is using blurred wallpaper for letterbox background. + && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0; + + mLastShouldShowLetterboxUi = shouldShowLetterboxUi; + + return shouldShowLetterboxUi; + } + + @VisibleForTesting + @Nullable + Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow) { + if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { + // We don't want corner radius on the window. + // In the case the ActivityRecord requires a letterboxed animation we never want + // rounded corners on the window because rounded corners are applied at the + // animation-bounds surface level and rounded corners on the window would interfere + // with that leading to unexpected rounded corner positioning during the animation. + return null; + } + + final Rect cropBounds = new Rect(mActivityRecord.getBounds()); + + // In case of translucent activities we check if the requested size is different from + // the size provided using inherited bounds. In that case we decide to not apply rounded + // corners because we assume the specific layout would. This is the case when the layout + // of the translucent activity uses only a part of all the bounds because of the use of + // LayoutParams.WRAP_CONTENT. + final TransparentPolicy transparentPolicy = mActivityRecord.mAppCompatController + .getTransparentPolicy(); + if (transparentPolicy.isRunning() && (cropBounds.width() != mainWindow.mRequestedWidth + || cropBounds.height() != mainWindow.mRequestedHeight)) { + return null; + } + + // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo} + // because taskbar bounds used in {@link #adjustBoundsIfNeeded} + // are in screen coordinates + adjustBoundsForTaskbar(mainWindow, cropBounds); + + final float scale = mainWindow.mInvGlobalScale; + if (scale != 1f && scale > 0f) { + cropBounds.scale(scale); + } + + // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface + // control is in the top left corner of an app window so offsetting bounds + // accordingly. + cropBounds.offsetTo(0, 0); + return cropBounds; + } + + + // Returns rounded corners radius the letterboxed activity should have based on override in + // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii. + // Device corners can be different on the right and left sides, but we use the same radius + // for all corners for consistency and pick a minimal bottom one for consistency with a + // taskbar rounded corners. + int getRoundedCornersRadius(@NonNull final WindowState mainWindow) { + if (!requiresRoundedCorners(mainWindow)) { + return 0; + } + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + final int radius; + if (letterboxOverrides.getLetterboxActivityCornersRadius() >= 0) { + radius = letterboxOverrides.getLetterboxActivityCornersRadius(); + } else { + final InsetsState insetsState = mainWindow.getInsetsState(); + radius = Math.min( + getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), + getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); + } + + final float scale = mainWindow.mInvGlobalScale; + return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius; + } + + void adjustBoundsForTaskbar(@NonNull final WindowState mainWindow, + @NonNull final Rect bounds) { + // Rounded corners should be displayed above the taskbar. When taskbar is hidden, + // an insets frame is equal to a navigation bar which shouldn't affect position of + // rounded corners since apps are expected to handle navigation bar inset. + // This condition checks whether the taskbar is visible. + // Do not crop the taskbar inset if the window is in immersive mode - the user can + // swipe to show/hide the taskbar as an overlay. + // Adjust the bounds only in case there is an expanded taskbar, + // otherwise the rounded corners will be shown behind the navbar. + final InsetsSource expandedTaskbarOrNull = + AppCompatUtils.getExpandedTaskbarOrNull(mainWindow); + if (expandedTaskbarOrNull != null) { + // Rounded corners should be displayed above the expanded taskbar. + bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top); + } + } + + private int getInsetsStateCornerRadius(@NonNull InsetsState insetsState, + @RoundedCorner.Position int position) { + final RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position); + return corner == null ? 0 : corner.getRadius(); + } + + private void updateWallpaperForLetterbox(@NonNull WindowState mainWindow) { + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + final @LetterboxBackgroundType int letterboxBackgroundType = + letterboxOverrides.getLetterboxBackgroundType(); + boolean wallpaperShouldBeShown = + letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER + // Don't use wallpaper as a background if letterboxed for display cutout. + && isLetterboxedNotForDisplayCutout(mainWindow) + // Check that dark scrim alpha or blur radius are provided + && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() > 0 + || letterboxOverrides.getLetterboxWallpaperDarkScrimAlpha() > 0) + // Check that blur is supported by a device if blur radius is provided. + && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() <= 0 + || letterboxOverrides.isLetterboxWallpaperBlurSupported()); + if (letterboxOverrides.checkWallpaperBackgroundForLetterbox(wallpaperShouldBeShown)) { + mActivityRecord.requestUpdateWallpaperIfNeeded(); + } + } + + void updateRoundedCornersIfNeeded(@NonNull final WindowState mainWindow) { + final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); + if (windowSurface == null || !windowSurface.isValid()) { + return; + } + + // cropBounds must be non-null for the cornerRadius to be ever applied. + mActivityRecord.getSyncTransaction() + .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow)) + .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); + } + + private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow) { + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + return isLetterboxedNotForDisplayCutout(mainWindow) + && letterboxOverrides.isLetterboxActivityCornersRounded(); + } + + private boolean isLetterboxedNotForDisplayCutout(@NonNull WindowState mainWindow) { + return shouldShowLetterboxUi(mainWindow) + && !mainWindow.isLetterboxedForDisplayCutout(); + } + + private static boolean shouldNotLayoutLetterbox(@Nullable WindowState w) { + if (w == null) { + return true; + } + final int type = w.mAttrs.type; + // Allow letterbox to be displayed early for base application or application starting + // windows even if it is not on the top z order to prevent flickering when the + // letterboxed window is brought to the top + return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING) + || w.mAnimatingExit; + } + + private class LetterboxPolicyState { + + @Nullable + private Letterbox mLetterbox; + + void layoutLetterboxIfNeeded(@NonNull WindowState w) { + if (!isRunning()) { + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord + .mAppCompatController.getAppCompatReachabilityPolicy(); + mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), + mActivityRecord.mWmService.mTransactionFactory, + reachabilityPolicy, letterboxOverrides, + this::getLetterboxParentSurface); + mLetterbox.attachInput(w); + mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame); + } + final Point letterboxPosition = new Point(); + if (mActivityRecord.isInLetterboxAnimation()) { + // In this case we attach the letterbox to the task instead of the activity. + mActivityRecord.getTask().getPosition(letterboxPosition); + } else { + mActivityRecord.getPosition(letterboxPosition); + } + + // Get the bounds of the "space-to-fill". The transformed bounds have the highest + // priority because the activity is launched in a rotated environment. In multi-window + // mode, the taskFragment-level represents this for both split-screen + // and activity-embedding. In fullscreen-mode, the task container does + // (since the orientation letterbox is also applied to the task). + final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); + final Rect spaceToFill = transformedBounds != null + ? transformedBounds + : mActivityRecord.inMultiWindowMode() + ? mActivityRecord.getTaskFragment().getBounds() + : mActivityRecord.getRootTask().getParent().getBounds(); + // In case of translucent activities an option is to use the WindowState#getFrame() of + // the first opaque activity beneath. In some cases (e.g. an opaque activity is using + // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct + // information and in particular it might provide a value for a smaller area making + // the letterbox overlap with the translucent activity's frame. + // If we use WindowState#getFrame() for the translucent activity's letterbox inner + // frame, the letterbox will then be overlapped with the translucent activity's frame. + // Because the surface layer of letterbox is lower than an activity window, this + // won't crop the content, but it may affect other features that rely on values stored + // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher + // For this reason we use ActivityRecord#getBounds() that the translucent activity + // inherits from the first opaque activity beneath and also takes care of the scaling + // in case of activities in size compat mode. + final TransparentPolicy transparentPolicy = + mActivityRecord.mAppCompatController.getTransparentPolicy(); + final Rect innerFrame = + transparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame(); + mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition); + if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides() + .isDoubleTapEvent()) { + // We need to notify Shell that letterbox position has changed. + mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + } + } + + /** + * @return {@code true} if the policy is running and so if the current activity is + * letterboxed. + */ + boolean isRunning() { + return mLetterbox != null; + } + + void onMovedToDisplay(int displayId) { + if (isRunning()) { + mLetterbox.onMovedToDisplay(displayId); + } + } + + /** Cleans up {@link Letterbox} if it exists.*/ + void destroy() { + if (isRunning()) { + mLetterbox.destroy(); + mLetterbox = null; + } + mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + .setLetterboxInnerBoundsSupplier(null); + } + + void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint, + @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction inputT) { + if (shouldNotLayoutLetterbox(winHint)) { + return; + } + start(winHint); + if (isRunning() && mLetterbox.needsApplySurfaceChanges()) { + mLetterbox.applySurfaceChanges(t, inputT); + } + } + + void hide() { + if (isRunning()) { + mLetterbox.hide(); + } + } + + /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ + @NonNull + Rect getLetterboxInsets() { + if (isRunning()) { + return mLetterbox.getInsets(); + } else { + return new Rect(); + } + } + + /** Gets the inner bounds of letterbox. The bounds will be empty with no letterbox. */ + void getLetterboxInnerBounds(@NonNull Rect outBounds) { + if (isRunning()) { + outBounds.set(mLetterbox.getInnerFrame()); + final WindowState w = mActivityRecord.findMainWindow(); + if (w != null) { + adjustBoundsForTaskbar(w, outBounds); + } + } else { + outBounds.setEmpty(); + } + } + + /** Gets the outer bounds of letterbox. The bounds will be empty with no letterbox. */ + private void getLetterboxOuterBounds(@NonNull Rect outBounds) { + if (isRunning()) { + outBounds.set(mLetterbox.getOuterFrame()); + } else { + outBounds.setEmpty(); + } + } + + /** + * @return {@code true} if bar shown within a given rectangle is allowed to be fully + * transparent when the current activity is displayed. + */ + boolean isFullyTransparentBarAllowed(@NonNull Rect rect) { + return !isRunning() || mLetterbox.notIntersectsOrFullyContains(rect); + } + + @Nullable + LetterboxDetails getLetterboxDetails() { + final WindowState w = mActivityRecord.findMainWindow(); + if (!isRunning() || w == null || w.isLetterboxedForDisplayCutout()) { + return null; + } + final Rect letterboxInnerBounds = new Rect(); + final Rect letterboxOuterBounds = new Rect(); + getLetterboxInnerBounds(letterboxInnerBounds); + getLetterboxOuterBounds(letterboxOuterBounds); + + if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) { + return null; + } + + return new LetterboxDetails( + letterboxInnerBounds, + letterboxOuterBounds, + w.mAttrs.insetsFlags.appearance + ); + } + + @Nullable + private SurfaceControl getLetterboxParentSurface() { + if (mActivityRecord.isInLetterboxAnimation()) { + return mActivityRecord.getTask().getSurfaceControl(); + } + return mActivityRecord.getSurfaceControl(); + } + + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 80bbee3dd78d..2f03105846bd 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -37,6 +37,8 @@ public class AppCompatOverrides { private final AppCompatResizeOverrides mAppCompatResizeOverrides; @NonNull private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; + @NonNull + private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; AppCompatOverrides(@NonNull ActivityRecord activityRecord, @NonNull AppCompatConfiguration appCompatConfiguration, @@ -54,6 +56,8 @@ public class AppCompatOverrides { mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord, appCompatConfiguration, optPropBuilder); mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder); + mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord, + appCompatConfiguration); } @NonNull @@ -85,4 +89,9 @@ public class AppCompatOverrides { AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { return mAppCompatReachabilityOverrides; } + + @NonNull + AppCompatLetterboxOverrides getAppCompatLetterboxOverrides() { + return mAppCompatLetterboxOverrides; + } } diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 0244d27f4363..91205fc757ad 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -28,6 +28,9 @@ import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.WindowInsets; import java.util.function.BooleanSupplier; @@ -212,6 +215,23 @@ class AppCompatUtils { return "UNKNOWN_REASON"; } + /** + * Returns the taskbar in case it is visible and expanded in height, otherwise returns null. + */ + @Nullable + static InsetsSource getExpandedTaskbarOrNull(@NonNull final WindowState mainWindow) { + final InsetsState state = mainWindow.getInsetsState(); + for (int i = state.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = state.sourceAt(i); + if (source.getType() == WindowInsets.Type.navigationBars() + && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER) + && source.isVisible()) { + return source; + } + } + return null; + } + private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) { info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index e18ca8552e72..5d8a96c530ef 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -51,6 +51,7 @@ import static com.android.server.wm.KeyguardControllerProto.KEYGUARD_SHOWING; import android.annotation.Nullable; import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemClock; import android.os.Trace; import android.util.Slog; import android.util.SparseArray; @@ -619,7 +620,8 @@ class KeyguardController { } state.mKeyguardGoingAway = false; state.writeEventLog("goingAwayTimeout"); - mWindowManager.mPolicy.startKeyguardExitAnimation(0); + mWindowManager.mPolicy.startKeyguardExitAnimation( + SystemClock.uptimeMillis() - GOING_AWAY_TIMEOUT_MS); } }; diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java index 3fc5eafc8737..252590e0b696 100644 --- a/services/core/java/com/android/server/wm/Letterbox.java +++ b/services/core/java/com/android/server/wm/Letterbox.java @@ -38,9 +38,6 @@ import android.view.WindowManager; import com.android.server.UiThread; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.IntSupplier; import java.util.function.Supplier; /** @@ -54,12 +51,6 @@ public class Letterbox { private final Supplier<SurfaceControl.Builder> mSurfaceControlFactory; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; - private final BooleanSupplier mAreCornersRounded; - private final Supplier<Color> mColorSupplier; - // Parameters for "blurred wallpaper" letterbox background. - private final BooleanSupplier mHasWallpaperBackgroundSupplier; - private final IntSupplier mBlurRadiusSupplier; - private final DoubleSupplier mDarkScrimAlphaSupplier; private final Supplier<SurfaceControl> mParentSurfaceSupplier; private final Rect mOuter = new Rect(); @@ -77,6 +68,8 @@ public class Letterbox { private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom }; @NonNull private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; + @NonNull + private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; /** * Constructs a Letterbox. @@ -85,24 +78,14 @@ public class Letterbox { */ public Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory, Supplier<SurfaceControl.Transaction> transactionFactory, - BooleanSupplier areCornersRounded, - Supplier<Color> colorSupplier, - BooleanSupplier hasWallpaperBackgroundSupplier, - IntSupplier blurRadiusSupplier, - DoubleSupplier darkScrimAlphaSupplier, @NonNull AppCompatReachabilityPolicy appCompatReachabilityPolicy, + @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides, Supplier<SurfaceControl> parentSurface) { mSurfaceControlFactory = surfaceControlFactory; mTransactionFactory = transactionFactory; - mAreCornersRounded = areCornersRounded; - mColorSupplier = colorSupplier; - mHasWallpaperBackgroundSupplier = hasWallpaperBackgroundSupplier; - mBlurRadiusSupplier = blurRadiusSupplier; - mDarkScrimAlphaSupplier = darkScrimAlphaSupplier; mAppCompatReachabilityPolicy = appCompatReachabilityPolicy; + mAppCompatLetterboxOverrides = appCompatLetterboxOverrides; mParentSurfaceSupplier = parentSurface; - // TODO Remove after Letterbox refactoring. - mAppCompatReachabilityPolicy.setLetterboxInnerBoundsSupplier(this::getInnerFrame); } /** @@ -252,7 +235,8 @@ public class Letterbox { * Returns {@code true} when using {@link #mFullWindowSurface} instead of {@link mSurfaces}. */ private boolean useFullWindowSurface() { - return mAreCornersRounded.getAsBoolean() || mHasWallpaperBackgroundSupplier.getAsBoolean(); + return mAppCompatLetterboxOverrides.shouldLetterboxHaveRoundedCorners() + || mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox(); } private final class TapEventReceiver extends InputEventReceiver { @@ -431,7 +415,7 @@ public class Letterbox { createSurface(t); } - mColor = mColorSupplier.get(); + mColor = mAppCompatLetterboxOverrides.getLetterboxBackgroundColor(); mParentSurface = mParentSurfaceSupplier.get(); t.setColor(mSurface, getRgbColorArray()); t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top); @@ -439,7 +423,8 @@ public class Letterbox { mSurfaceFrameRelative.height()); t.reparent(mSurface, mParentSurface); - mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.getAsBoolean(); + mHasWallpaperBackground = mAppCompatLetterboxOverrides + .hasWallpaperBackgroundForLetterbox(); updateAlphaAndBlur(t); t.show(mSurface); @@ -460,17 +445,19 @@ public class Letterbox { t.setBackgroundBlurRadius(mSurface, 0); return; } - final float alpha = (float) mDarkScrimAlphaSupplier.getAsDouble(); + final float alpha = mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha(); t.setAlpha(mSurface, alpha); // Translucent dark scrim can be shown without blur. - if (mBlurRadiusSupplier.getAsInt() <= 0) { + final int blurRadiusPx = mAppCompatLetterboxOverrides + .getLetterboxWallpaperBlurRadiusPx(); + if (blurRadiusPx <= 0) { // Removing pre-exesting blur t.setBackgroundBlurRadius(mSurface, 0); return; } - t.setBackgroundBlurRadius(mSurface, mBlurRadiusSupplier.getAsInt()); + t.setBackgroundBlurRadius(mSurface, blurRadiusPx); } private float[] getRgbColorArray() { @@ -487,8 +474,9 @@ public class Letterbox { // and mParentSurface may never be updated in applySurfaceChanges but this // doesn't mean that update is needed. || !mSurfaceFrameRelative.isEmpty() - && (mHasWallpaperBackgroundSupplier.getAsBoolean() != mHasWallpaperBackground - || !mColorSupplier.get().equals(mColor) + && (mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox() + != mHasWallpaperBackground + || !mAppCompatLetterboxOverrides.getLetterboxBackgroundColor().equals(mColor) || mParentSurfaceSupplier.get() != mParentSurface); } } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 4740fc45c6ba..0e33734fc7a5 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -16,36 +16,19 @@ package com.android.server.wm; -import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; - import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager.TaskDescription; import android.graphics.Color; -import android.graphics.Point; import android.graphics.Rect; -import android.util.Slog; -import android.view.InsetsSource; -import android.view.InsetsState; -import android.view.RoundedCorner; -import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; -import android.view.WindowInsets; -import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.LetterboxDetails; -import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; import java.io.PrintWriter; @@ -56,8 +39,6 @@ final class LetterboxUiController { private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; - private final Point mTmpPoint = new Point(); - private final AppCompatConfiguration mAppCompatConfiguration; private final ActivityRecord mActivityRecord; @@ -66,18 +47,9 @@ final class LetterboxUiController { @NonNull private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; @NonNull - private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; - @NonNull - private final TransparentPolicy mTransparentPolicy; + private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; @NonNull - private final AppCompatOrientationOverrides mAppCompatOrientationOverrides; - - private boolean mShowWallpaperForLetterboxBackground; - - @Nullable - private Letterbox mLetterbox; - - private boolean mLastShouldShowLetterboxUi; + private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { mAppCompatConfiguration = wmService.mAppCompatConfiguration; @@ -88,63 +60,34 @@ final class LetterboxUiController { // TODO(b/356385137): Remove these we added to make dependencies temporarily explicit. mAppCompatReachabilityOverrides = mActivityRecord.mAppCompatController .getAppCompatReachabilityOverrides(); - mAppCompatReachabilityPolicy = mActivityRecord.mAppCompatController - .getAppCompatReachabilityPolicy(); - mTransparentPolicy = mActivityRecord.mAppCompatController.getTransparentPolicy(); - mAppCompatOrientationOverrides = mActivityRecord.mAppCompatController - .getAppCompatOrientationOverrides(); + mAppCompatLetterboxPolicy = mActivityRecord.mAppCompatController + .getAppCompatLetterboxPolicy(); + mAppCompatLetterboxOverrides = mActivityRecord.mAppCompatController + .getAppCompatLetterboxOverrides(); } /** Cleans up {@link Letterbox} if it exists.*/ void destroy() { - if (mLetterbox != null) { - mLetterbox.destroy(); - mLetterbox = null; - // TODO Remove after Letterbox refactoring. - mAppCompatReachabilityPolicy.setLetterboxInnerBoundsSupplier(null); - } + mAppCompatLetterboxPolicy.destroy(); } void onMovedToDisplay(int displayId) { - if (mLetterbox != null) { - mLetterbox.onMovedToDisplay(displayId); - } + mAppCompatLetterboxPolicy.onMovedToDisplay(displayId); } boolean hasWallpaperBackgroundForLetterbox() { - return mShowWallpaperForLetterboxBackground; + return mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox(); } /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ Rect getLetterboxInsets() { - if (mLetterbox != null) { - return mLetterbox.getInsets(); - } else { - return new Rect(); - } + return mAppCompatLetterboxPolicy.getLetterboxInsets(); } /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */ void getLetterboxInnerBounds(Rect outBounds) { - if (mLetterbox != null) { - outBounds.set(mLetterbox.getInnerFrame()); - final WindowState w = mActivityRecord.findMainWindow(); - if (w != null) { - adjustBoundsForTaskbar(w, outBounds); - } - } else { - outBounds.setEmpty(); - } - } - - /** Gets the outer bounds of letterbox. The bounds will be empty if there is no letterbox. */ - private void getLetterboxOuterBounds(Rect outBounds) { - if (mLetterbox != null) { - outBounds.set(mLetterbox.getOuterFrame()); - } else { - outBounds.setEmpty(); - } + mAppCompatLetterboxPolicy.getLetterboxInnerBounds(outBounds); } /** @@ -152,234 +95,39 @@ final class LetterboxUiController { * when the current activity is displayed. */ boolean isFullyTransparentBarAllowed(Rect rect) { - return mLetterbox == null || mLetterbox.notIntersectsOrFullyContains(rect); + return mAppCompatLetterboxPolicy.isFullyTransparentBarAllowed(rect); } void updateLetterboxSurfaceIfNeeded(WindowState winHint) { - updateLetterboxSurfaceIfNeeded(winHint, mActivityRecord.getSyncTransaction(), - mActivityRecord.getPendingTransaction()); + mAppCompatLetterboxPolicy.updateLetterboxSurfaceIfNeeded(winHint); } void updateLetterboxSurfaceIfNeeded(WindowState winHint, @NonNull Transaction t, @NonNull Transaction inputT) { - if (shouldNotLayoutLetterbox(winHint)) { - return; - } - layoutLetterboxIfNeeded(winHint); - if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) { - mLetterbox.applySurfaceChanges(t, inputT); - } + mAppCompatLetterboxPolicy.updateLetterboxSurfaceIfNeeded(winHint, t, inputT); } void layoutLetterboxIfNeeded(WindowState w) { - if (shouldNotLayoutLetterbox(w)) { - return; - } - updateRoundedCornersIfNeeded(w); - updateWallpaperForLetterbox(w); - if (shouldShowLetterboxUi(w)) { - if (mLetterbox == null) { - mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), - mActivityRecord.mWmService.mTransactionFactory, - this::shouldLetterboxHaveRoundedCorners, - this::getLetterboxBackgroundColor, - this::hasWallpaperBackgroundForLetterbox, - this::getLetterboxWallpaperBlurRadiusPx, - this::getLetterboxWallpaperDarkScrimAlpha, - mAppCompatReachabilityPolicy, - this::getLetterboxParentSurface); - mLetterbox.attachInput(w); - } - - if (mActivityRecord.isInLetterboxAnimation()) { - // In this case we attach the letterbox to the task instead of the activity. - mActivityRecord.getTask().getPosition(mTmpPoint); - } else { - mActivityRecord.getPosition(mTmpPoint); - } - - // Get the bounds of the "space-to-fill". The transformed bounds have the highest - // priority because the activity is launched in a rotated environment. In multi-window - // mode, the taskFragment-level represents this for both split-screen - // and activity-embedding. In fullscreen-mode, the task container does - // (since the orientation letterbox is also applied to the task). - final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); - final Rect spaceToFill = transformedBounds != null - ? transformedBounds - : mActivityRecord.inMultiWindowMode() - ? mActivityRecord.getTaskFragment().getBounds() - : mActivityRecord.getRootTask().getParent().getBounds(); - // In case of translucent activities an option is to use the WindowState#getFrame() of - // the first opaque activity beneath. In some cases (e.g. an opaque activity is using - // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct - // information and in particular it might provide a value for a smaller area making - // the letterbox overlap with the translucent activity's frame. - // If we use WindowState#getFrame() for the translucent activity's letterbox inner - // frame, the letterbox will then be overlapped with the translucent activity's frame. - // Because the surface layer of letterbox is lower than an activity window, this - // won't crop the content, but it may affect other features that rely on values stored - // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher - // For this reason we use ActivityRecord#getBounds() that the translucent activity - // inherits from the first opaque activity beneath and also takes care of the scaling - // in case of activities in size compat mode. - final Rect innerFrame = - mTransparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame(); - mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint); - if (mAppCompatReachabilityOverrides.isDoubleTapEvent()) { - // We need to notify Shell that letterbox position has changed. - mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); - } - } else if (mLetterbox != null) { - mLetterbox.hide(); - } - } - - SurfaceControl getLetterboxParentSurface() { - if (mActivityRecord.isInLetterboxAnimation()) { - return mActivityRecord.getTask().getSurfaceControl(); - } - return mActivityRecord.getSurfaceControl(); - } - - private static boolean shouldNotLayoutLetterbox(WindowState w) { - if (w == null) { - return true; - } - final int type = w.mAttrs.type; - // Allow letterbox to be displayed early for base application or application starting - // windows even if it is not on the top z order to prevent flickering when the - // letterboxed window is brought to the top - return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING) - || w.mAnimatingExit; - } - - private boolean shouldLetterboxHaveRoundedCorners() { - // TODO(b/214030873): remove once background is drawn for transparent activities - // Letterbox shouldn't have rounded corners if the activity is transparent - return mAppCompatConfiguration.isLetterboxActivityCornersRounded() - && mActivityRecord.fillsParent(); + mAppCompatLetterboxPolicy.start(w); } boolean isLetterboxEducationEnabled() { - return mAppCompatConfiguration.getIsEducationEnabled(); + return mAppCompatLetterboxOverrides.isLetterboxEducationEnabled(); } @VisibleForTesting boolean shouldShowLetterboxUi(WindowState mainWindow) { - if (mAppCompatOrientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) { - return mLastShouldShowLetterboxUi; - } - - final boolean shouldShowLetterboxUi = - (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible() - || mActivityRecord.isVisibleRequested()) - && mainWindow.areAppWindowBoundsLetterboxed() - // Check for FLAG_SHOW_WALLPAPER explicitly instead of using - // WindowContainer#showWallpaper because the later will return true when this - // activity is using blurred wallpaper for letterbox background. - && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0; - - mLastShouldShowLetterboxUi = shouldShowLetterboxUi; - - return shouldShowLetterboxUi; + return mAppCompatLetterboxPolicy.shouldShowLetterboxUi(mainWindow); } Color getLetterboxBackgroundColor() { - final WindowState w = mActivityRecord.findMainWindow(); - if (w == null || w.isLetterboxedForDisplayCutout()) { - return Color.valueOf(Color.BLACK); - } - @LetterboxBackgroundType int letterboxBackgroundType = - mAppCompatConfiguration.getLetterboxBackgroundType(); - TaskDescription taskDescription = mActivityRecord.taskDescription; - switch (letterboxBackgroundType) { - case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: - if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) { - return Color.valueOf(taskDescription.getBackgroundColorFloating()); - } - break; - case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: - if (taskDescription != null && taskDescription.getBackgroundColor() != 0) { - return Color.valueOf(taskDescription.getBackgroundColor()); - } - break; - case LETTERBOX_BACKGROUND_WALLPAPER: - if (hasWallpaperBackgroundForLetterbox()) { - // Color is used for translucent scrim that dims wallpaper. - return mAppCompatConfiguration.getLetterboxBackgroundColor(); - } - Slog.w(TAG, "Wallpaper option is selected for letterbox background but " - + "blur is not supported by a device or not supported in the current " - + "window configuration or both alpha scrim and blur radius aren't " - + "provided so using solid color background"); - break; - case LETTERBOX_BACKGROUND_SOLID_COLOR: - return mAppCompatConfiguration.getLetterboxBackgroundColor(); - default: - throw new AssertionError( - "Unexpected letterbox background type: " + letterboxBackgroundType); - } - // If picked option configured incorrectly or not supported then default to a solid color - // background. - return mAppCompatConfiguration.getLetterboxBackgroundColor(); - } - - private void updateRoundedCornersIfNeeded(final WindowState mainWindow) { - final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); - if (windowSurface == null || !windowSurface.isValid()) { - return; - } - - // cropBounds must be non-null for the cornerRadius to be ever applied. - mActivityRecord.getSyncTransaction() - .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow)) - .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); + return mAppCompatLetterboxOverrides.getLetterboxBackgroundColor(); } @VisibleForTesting @Nullable Rect getCropBoundsIfNeeded(final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { - // We don't want corner radius on the window. - // In the case the ActivityRecord requires a letterboxed animation we never want - // rounded corners on the window because rounded corners are applied at the - // animation-bounds surface level and rounded corners on the window would interfere - // with that leading to unexpected rounded corner positioning during the animation. - return null; - } - - final Rect cropBounds = new Rect(mActivityRecord.getBounds()); - - // In case of translucent activities we check if the requested size is different from - // the size provided using inherited bounds. In that case we decide to not apply rounded - // corners because we assume the specific layout would. This is the case when the layout - // of the translucent activity uses only a part of all the bounds because of the use of - // LayoutParams.WRAP_CONTENT. - if (mTransparentPolicy.isRunning() && (cropBounds.width() != mainWindow.mRequestedWidth - || cropBounds.height() != mainWindow.mRequestedHeight)) { - return null; - } - - // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo} - // because taskbar bounds used in {@link #adjustBoundsIfNeeded} - // are in screen coordinates - adjustBoundsForTaskbar(mainWindow, cropBounds); - - final float scale = mainWindow.mInvGlobalScale; - if (scale != 1f && scale > 0f) { - cropBounds.scale(scale); - } - - // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface - // control is in the top left corner of an app window so offsetting bounds - // accordingly. - cropBounds.offsetTo(0, 0); - return cropBounds; - } - - private boolean requiresRoundedCorners(final WindowState mainWindow) { - return isLetterboxedNotForDisplayCutout(mainWindow) - && mAppCompatConfiguration.isLetterboxActivityCornersRounded(); + return mAppCompatLetterboxPolicy.getCropBoundsIfNeeded(mainWindow); } // Returns rounded corners radius the letterboxed activity should have based on override in @@ -388,102 +136,12 @@ final class LetterboxUiController { // for all corners for consistency and pick a minimal bottom one for consistency with a // taskbar rounded corners. int getRoundedCornersRadius(final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow)) { - return 0; - } - - final int radius; - if (mAppCompatConfiguration.getLetterboxActivityCornersRadius() >= 0) { - radius = mAppCompatConfiguration.getLetterboxActivityCornersRadius(); - } else { - final InsetsState insetsState = mainWindow.getInsetsState(); - radius = Math.min( - getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), - getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); - } - - final float scale = mainWindow.mInvGlobalScale; - return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius; + return mAppCompatLetterboxPolicy.getRoundedCornersRadius(mainWindow); } - /** - * Returns the taskbar in case it is visible and expanded in height, otherwise returns null. - */ - @VisibleForTesting @Nullable - InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) { - final InsetsState state = mainWindow.getInsetsState(); - for (int i = state.sourceSize() - 1; i >= 0; i--) { - final InsetsSource source = state.sourceAt(i); - if (source.getType() == WindowInsets.Type.navigationBars() - && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER) - && source.isVisible()) { - return source; - } - } - return null; - } - - private void adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds) { - // Rounded corners should be displayed above the taskbar. When taskbar is hidden, - // an insets frame is equal to a navigation bar which shouldn't affect position of - // rounded corners since apps are expected to handle navigation bar inset. - // This condition checks whether the taskbar is visible. - // Do not crop the taskbar inset if the window is in immersive mode - the user can - // swipe to show/hide the taskbar as an overlay. - // Adjust the bounds only in case there is an expanded taskbar, - // otherwise the rounded corners will be shown behind the navbar. - final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow); - if (expandedTaskbarOrNull != null) { - // Rounded corners should be displayed above the expanded taskbar. - bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top); - } - } - - private int getInsetsStateCornerRadius( - InsetsState insetsState, @RoundedCorner.Position int position) { - RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position); - return corner == null ? 0 : corner.getRadius(); - } - - private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) { - return shouldShowLetterboxUi(mainWindow) - && !mainWindow.isLetterboxedForDisplayCutout(); - } - - private void updateWallpaperForLetterbox(WindowState mainWindow) { - @LetterboxBackgroundType int letterboxBackgroundType = - mAppCompatConfiguration.getLetterboxBackgroundType(); - boolean wallpaperShouldBeShown = - letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER - // Don't use wallpaper as a background if letterboxed for display cutout. - && isLetterboxedNotForDisplayCutout(mainWindow) - // Check that dark scrim alpha or blur radius are provided - && (getLetterboxWallpaperBlurRadiusPx() > 0 - || getLetterboxWallpaperDarkScrimAlpha() > 0) - // Check that blur is supported by a device if blur radius is provided. - && (getLetterboxWallpaperBlurRadiusPx() <= 0 - || isLetterboxWallpaperBlurSupported()); - if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) { - mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown; - mActivityRecord.requestUpdateWallpaperIfNeeded(); - } - } - - private int getLetterboxWallpaperBlurRadiusPx() { - int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx(); - return Math.max(blurRadius, 0); - } - - private float getLetterboxWallpaperDarkScrimAlpha() { - float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); - // No scrim by default. - return (alpha < 0 || alpha >= 1) ? 0.0f : alpha; - } - - private boolean isLetterboxWallpaperBlurSupported() { - return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class) - .isCrossWindowBlurEnabled(); + LetterboxDetails getLetterboxDetails() { + return mAppCompatLetterboxPolicy.getLetterboxDetails(); } void dump(PrintWriter pw, String prefix) { @@ -526,11 +184,11 @@ final class LetterboxUiController { if (mAppCompatConfiguration.getLetterboxBackgroundType() == LETTERBOX_BACKGROUND_WALLPAPER) { pw.println(prefix + " isLetterboxWallpaperBlurSupported=" - + isLetterboxWallpaperBlurSupported()); + + mAppCompatLetterboxOverrides.isLetterboxWallpaperBlurSupported()); pw.println(prefix + " letterboxBackgroundWallpaperDarkScrimAlpha=" - + getLetterboxWallpaperDarkScrimAlpha()); + + mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha()); pw.println(prefix + " letterboxBackgroundWallpaperBlurRadius=" - + getLetterboxWallpaperBlurRadiusPx()); + + mAppCompatLetterboxOverrides.getLetterboxWallpaperBlurRadiusPx()); } final AppCompatReachabilityOverrides reachabilityOverrides = mActivityRecord .mAppCompatController.getAppCompatReachabilityOverrides(); @@ -560,26 +218,4 @@ final class LetterboxUiController { + mAppCompatConfiguration .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()); } - - @Nullable - LetterboxDetails getLetterboxDetails() { - final WindowState w = mActivityRecord.findMainWindow(); - if (mLetterbox == null || w == null || w.isLetterboxedForDisplayCutout()) { - return null; - } - Rect letterboxInnerBounds = new Rect(); - Rect letterboxOuterBounds = new Rect(); - getLetterboxInnerBounds(letterboxInnerBounds); - getLetterboxOuterBounds(letterboxOuterBounds); - - if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) { - return null; - } - - return new LetterboxDetails( - letterboxInnerBounds, - letterboxOuterBounds, - w.mAttrs.insetsFlags.appearance - ); - } } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 561ff7db5b9d..7212d379162b 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -407,8 +407,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr change.setTaskFragmentToken(lastParentTfToken); } // Only pass the activity token to the client if it belongs to the same process. - if (Flags.fixPipRestoreToOverlay() && nextFillTaskActivity != null - && nextFillTaskActivity.getPid() == mOrganizerPid) { + if (nextFillTaskActivity != null && nextFillTaskActivity.getPid() == mOrganizerPid) { change.setOtherActivityToken(nextFillTaskActivity.token); } return change; diff --git a/services/core/java/com/android/server/wm/WindowAnimationSpec.java b/services/core/java/com/android/server/wm/WindowAnimationSpec.java index 34b9913c9738..2c58c61701cc 100644 --- a/services/core/java/com/android/server/wm/WindowAnimationSpec.java +++ b/services/core/java/com/android/server/wm/WindowAnimationSpec.java @@ -97,10 +97,10 @@ public class WindowAnimationSpec implements AnimationSpec { /** * @return If a window animation has outsets applied to it. - * @see Animation#hasExtension() + * @see Animation#getExtensionEdges() */ public boolean hasExtension() { - return mAnimation.hasExtension(); + return mAnimation.getExtensionEdges() != 0; } @Override diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 58c48ad3e9ac..c6d0b729cd5a 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1194,7 +1194,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } } - effects |= sanitizeAndApplyHierarchyOp(wc, hop); + if (wc.asTask() != null) { + effects |= sanitizeAndApplyHierarchyOpForTask(wc.asTask(), hop); + } else if (wc.asDisplayArea() != null) { + effects |= sanitizeAndApplyHierarchyOpForDisplayArea(wc.asDisplayArea(), hop); + } else { + throw new IllegalArgumentException("Invalid container in hierarchy op"); + } break; } case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: { @@ -1850,12 +1856,22 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return starterResult[0]; } - private int sanitizeAndApplyHierarchyOp(WindowContainer container, - WindowContainerTransaction.HierarchyOp hop) { - final Task task = container.asTask(); - if (task == null) { - throw new IllegalArgumentException("Invalid container in hierarchy op"); + private int sanitizeAndApplyHierarchyOpForDisplayArea(@NonNull DisplayArea displayArea, + @NonNull WindowContainerTransaction.HierarchyOp hop) { + if (hop.getType() != HIERARCHY_OP_TYPE_REORDER) { + throw new UnsupportedOperationException("DisplayArea only supports reordering"); + } + if (displayArea.getParent() == null) { + return TRANSACT_EFFECTS_NONE; } + displayArea.getParent().positionChildAt( + hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM, + displayArea, hop.includingParents()); + return TRANSACT_EFFECTS_LIFECYCLE; + } + + private int sanitizeAndApplyHierarchyOpForTask(@NonNull Task task, + @NonNull WindowContainerTransaction.HierarchyOp hop) { final DisplayContent dc = task.getDisplayContent(); if (dc == null) { Slog.w(TAG, "Container is no longer attached: " + task); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 9e8811f419a2..09c54cb40373 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -16,6 +16,7 @@ package com.android.server; +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_HIGH; @@ -105,6 +106,7 @@ import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.BinderInternal; import com.android.internal.os.RuntimeInit; import com.android.internal.policy.AttributeCache; +import com.android.internal.protolog.ProtoLogService; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.util.FrameworkStatsLog; @@ -119,6 +121,7 @@ import com.android.server.am.ActivityManagerService; import com.android.server.ambientcontext.AmbientContextManagerService; import com.android.server.app.GameManagerService; import com.android.server.appbinding.AppBindingService; +import com.android.server.appfunctions.AppFunctionManagerService; import com.android.server.apphibernation.AppHibernationService; import com.android.server.appop.AppOpMigrationHelper; import com.android.server.appop.AppOpMigrationHelperImpl; @@ -1087,6 +1090,13 @@ public final class SystemServer implements Dumpable { SystemServerInitThreadPool.submit(SystemConfig::getInstance, TAG_SYSTEM_CONFIG); t.traceEnd(); + // Orchestrates some ProtoLogging functionality. + if (android.tracing.Flags.clientSideProtoLogging()) { + t.traceBegin("StartProtoLogService"); + ServiceManager.addService(Context.PROTOLOG_SERVICE, new ProtoLogService()); + t.traceEnd(); + } + // Platform compat service is used by ActivityManagerService, PackageManagerService, and // possibly others in the future. b/135010838. t.traceBegin("PlatformCompat"); @@ -1719,6 +1729,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(LogcatManagerService.class); t.traceEnd(); + t.traceBegin("StartAppFunctionManager"); + if (enableAppFunctionManager()) { + mSystemServiceManager.startService(AppFunctionManagerService.class); + } + t.traceEnd(); + } catch (Throwable e) { Slog.e("System", "******************************************"); Slog.e("System", "************ Failure starting core service"); diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java index 0d14c9f02677..1b9f8d1f90c3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java @@ -40,6 +40,8 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AlarmManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -224,6 +226,45 @@ public class CountQuotaTrackerTest { } } + private LongArrayQueue getEvents(int userId, String packageName, String tag) { + synchronized (mQuotaTracker.mLock) { + return mQuotaTracker.getEvents(userId, packageName, tag); + } + } + + private void updateExecutionStats(final int userId, @NonNull final String packageName, + @Nullable final String tag, @NonNull ExecutionStats stats) { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.updateExecutionStatsLocked(userId, packageName, tag, stats); + } + } + + private ExecutionStats getExecutionStats(final int userId, @NonNull final String packageName, + @Nullable final String tag) { + synchronized (mQuotaTracker.mLock) { + return mQuotaTracker.getExecutionStatsLocked(userId, packageName, tag); + } + } + + private void maybeScheduleStartAlarm(final int userId, @NonNull final String packageName, + @Nullable final String tag) { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.maybeScheduleStartAlarmLocked(userId, packageName, tag); + } + } + + private void maybeScheduleCleanupAlarm() { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + } + } + + private void deleteObsoleteEvents() { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.deleteObsoleteEventsLocked(); + } + } + @Test public void testDeleteObsoleteEventsLocked() { // Count window size should only apply to event list. @@ -243,9 +284,9 @@ public class CountQuotaTrackerTest { expectedEvents.addLast(now - HOUR_IN_MILLIS); expectedEvents.addLast(now - 1); - mQuotaTracker.deleteObsoleteEventsLocked(); + deleteObsoleteEvents(); - LongArrayQueue remainingEvents = mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, + LongArrayQueue remainingEvents = getEvents(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertTrue(longArrayQueueEquals(expectedEvents, remainingEvents)); } @@ -270,15 +311,15 @@ public class CountQuotaTrackerTest { removal.putExtra(Intent.EXTRA_UID, TEST_UID); mReceiver.onReceive(mContext, removal); assertNull( - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag1")); + getEvents(TEST_USER_ID, "com.android.test.remove", "tag1")); assertNull( - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag2")); + getEvents(TEST_USER_ID, "com.android.test.remove", "tag2")); assertNull( - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag3")); + getEvents(TEST_USER_ID, "com.android.test.remove", "tag3")); assertTrue(longArrayQueueEquals(expected1, - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.stay", "tag1"))); + getEvents(TEST_USER_ID, "com.android.test.stay", "tag1"))); assertTrue(longArrayQueueEquals(expected2, - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.stay", "tag2"))); + getEvents(TEST_USER_ID, "com.android.test.stay", "tag2"))); } @Test @@ -298,10 +339,10 @@ public class CountQuotaTrackerTest { Intent removal = new Intent(Intent.ACTION_USER_REMOVED); removal.putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID); mReceiver.onReceive(mContext, removal); - assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag1")); - assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag2")); - assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag3")); - longArrayQueueEquals(expected, mQuotaTracker.getEvents(10, TEST_PACKAGE, "tag4")); + assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag1")); + assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag2")); + assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag3")); + longArrayQueueEquals(expected, getEvents(10, TEST_PACKAGE, "tag4")); } @Test @@ -323,7 +364,7 @@ public class CountQuotaTrackerTest { inputStats.countLimit = expectedStats.countLimit = 3; // Invalid time is now +24 hours since there are no sessions at all for the app. expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, "com.android.test.not.run", TEST_TAG, + updateExecutionStats(TEST_USER_ID, "com.android.test.not.run", TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); @@ -333,19 +374,19 @@ public class CountQuotaTrackerTest { // Invalid time is now since there was an event exactly windowSizeMs ago. expectedStats.expirationTimeElapsed = now; expectedStats.countInWindow = 1; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 2 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 1; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 4 * MINUTE_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 3 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 1; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS; @@ -353,13 +394,13 @@ public class CountQuotaTrackerTest { // minutes. expectedStats.expirationTimeElapsed = now + 44 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 2; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 45 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 2; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS; @@ -370,28 +411,28 @@ public class CountQuotaTrackerTest { // App is at event count limit but the oldest session is at the edge of the window, so // in quota time is now. expectedStats.inQuotaTimeElapsed = now; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.countInWindow = 3; expectedStats.inQuotaTimeElapsed = now + HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * HOUR_IN_MILLIS; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.countInWindow = 4; expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 2 * HOUR_IN_MILLIS; expectedStats.countInWindow = 4; expectedStats.inQuotaTimeElapsed = now + 5 * HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); } @@ -428,7 +469,7 @@ public class CountQuotaTrackerTest { expectedStats.countInWindow = 1; mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); // Working expectedStats.expirationTimeElapsed = now; @@ -437,7 +478,7 @@ public class CountQuotaTrackerTest { expectedStats.countInWindow = 2; mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); // Frequent expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; @@ -447,7 +488,7 @@ public class CountQuotaTrackerTest { expectedStats.inQuotaTimeElapsed = now + HOUR_IN_MILLIS; mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); // Rare expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; @@ -457,7 +498,7 @@ public class CountQuotaTrackerTest { expectedStats.inQuotaTimeElapsed = now + 19 * HOUR_IN_MILLIS; mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); } /** @@ -481,7 +522,7 @@ public class CountQuotaTrackerTest { expectedStats.countInWindow = 3; expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + 30_000; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); } @Test @@ -556,20 +597,20 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 5, 24 * HOUR_IN_MILLIS); // No sessions saved yet. - mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + maybeScheduleCleanupAlarm(); verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_CLEANUP), any(), any()); // Test with only one timing session saved. final long now = mInjector.getElapsedRealtime(); logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 6 * HOUR_IN_MILLIS); - mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + maybeScheduleCleanupAlarm(); verify(mAlarmManager, timeout(1000).times(1)) .set(anyInt(), eq(now + 18 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any()); // Test with new (more recent) timing sessions saved. AlarmManger shouldn't be called again. logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS); logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS); - mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + maybeScheduleCleanupAlarm(); verify(mAlarmManager, times(1)) .set(anyInt(), eq(now + 18 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any()); } @@ -587,14 +628,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 8 * HOUR_IN_MILLIS); // No sessions saved yet. - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = mInjector.getElapsedRealtime(); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); @@ -602,26 +643,26 @@ public class CountQuotaTrackerTest { final long start = now - (6 * HOUR_IN_MILLIS); final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS; logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); @@ -656,7 +697,7 @@ public class CountQuotaTrackerTest { // Start in ACTIVE bucket. mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, never()) .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); @@ -665,40 +706,40 @@ public class CountQuotaTrackerTest { // And down from there. final long expectedWorkingAlarmTime = outOfQuotaTime + (2 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // And back up again. mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .cancel(any(AlarmManager.OnAlarmListener.class)); inOrder.verify(mAlarmManager, timeout(1000).times(0)) @@ -745,14 +786,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(0, stats.countInWindow); } } @@ -766,14 +807,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(i + 1, stats.countInWindow); } } @@ -785,14 +826,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(0, stats.countInWindow); } } @@ -806,14 +847,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(i + 1, stats.countInWindow); } } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index dbab54b76a2e..a8e350b05f18 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -19,6 +19,8 @@ package com.android.server.am; import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; +import static android.app.ActivityManager.STOP_USER_ON_SWITCH_TRUE; +import static android.app.ActivityManager.STOP_USER_ON_SWITCH_FALSE; import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; @@ -201,7 +203,8 @@ public class UserControllerTest { doNothing().when(mInjector).systemServiceManagerOnUserStopped(anyInt()); doNothing().when(mInjector).systemServiceManagerOnUserCompletedEvent( anyInt(), anyInt()); - doNothing().when(mInjector).activityManagerForceStopPackage(anyInt(), anyString()); + doNothing().when(mInjector).activityManagerForceStopUserPackages(anyInt(), + anyString(), anyBoolean()); doNothing().when(mInjector).activityManagerOnUserStopped(anyInt()); doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt()); doNothing().when(mInjector).taskSupervisorRemoveUser(anyInt()); @@ -936,6 +939,61 @@ public class UserControllerTest { new HashSet<>(mUserController.getRunningUsersLU())); } + @Test + public void testEarlyPackageKillEnabledForUserSwitch_enabled() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + assertTrue(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_withoutDelayUserDataLocking() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ false, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + assertFalse(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_withPrevSystemUser() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + assertFalse(mUserController + .isEarlyPackageKillEnabledForUserSwitch(SYSTEM_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_stopUserOnSwitchModeOn() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ false, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + mUserController.setStopUserOnSwitch(STOP_USER_ON_SWITCH_TRUE); + + assertTrue(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_stopUserOnSwitchModeOff() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + mUserController.setStopUserOnSwitch(STOP_USER_ON_SWITCH_FALSE); + + assertFalse(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + /** * Test that, in getRunningUsersLU, parents come after their profile, even if the profile was * started afterwards. diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java index fad10f7a7553..551808243640 100644 --- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -182,7 +182,7 @@ public final class DeviceStateProviderImplTest { + " <device-state>\n" + " <identifier>1</identifier>\n" + " <properties>\n" - + " <property>PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS</property>\n" + + " <property>com.android.server.policy.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS</property>\n" + " </properties>\n" + " <conditions/>\n" + " </device-state>\n" @@ -338,11 +338,9 @@ public final class DeviceStateProviderImplTest { + " <identifier>4</identifier>\n" + " <name>THERMAL_TEST</name>\n" + " <properties>\n" - + " <property>PROPERTY_EMULATED_ONLY</property>\n" - + " <property>PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL" - + "</property>\n" - + " <property>PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE" - + "</property>\n" + + " <property>com.android.server.policy.PROPERTY_EMULATED_ONLY</property>\n" + + " <property>com.android.server.policy.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL</property>\n" + + " <property>com.android.server.policy.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE</property>\n" + " </properties>\n" + " </device-state>\n" + "</device-state-config>\n"; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java index f6e1162a5ada..af7f703e9c31 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java @@ -16,7 +16,6 @@ package com.android.server.notification; -import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; @@ -31,13 +30,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import android.app.Flags; import android.content.ComponentName; import android.content.ServiceConnection; import android.content.pm.IPackageManager; import android.net.Uri; import android.os.IInterface; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.Condition; @@ -150,57 +147,6 @@ public class ConditionProvidersTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_UI) - public void notifyConditions_appCannotUndoUserEnablement() { - ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo( - mock(IInterface.class), new ComponentName("package", "cls"), 0, false, - mock(ServiceConnection.class), 33, 100); - // First, user enabled mode - Condition[] userConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_TRUE, SOURCE_USER_ACTION) - }; - mProviders.notifyConditions("package", msi, userConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0])); - - // Second, app tries to disable it, but cannot - Condition[] appConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_FALSE) - }; - mProviders.notifyConditions("package", msi, appConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0])); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_UI) - public void notifyConditions_appCanTakeoverUserEnablement() { - ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo( - mock(IInterface.class), new ComponentName("package", "cls"), 0, false, - mock(ServiceConnection.class), 33, 100); - // First, user enabled mode - Condition[] userConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_TRUE, SOURCE_USER_ACTION) - }; - mProviders.notifyConditions("package", msi, userConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0])); - - // Second, app now thinks the rule should be on due it its intelligence - Condition[] appConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_TRUE) - }; - mProviders.notifyConditions("package", msi, appConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(appConditions[0])); - - // Lastly, app can turn rule off when its intelligence think it should be off - appConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_FALSE) - }; - mProviders.notifyConditions("package", msi, appConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(appConditions[0])); - - verifyNoMoreInteractions(mCallback); - } - - @Test public void testRemoveDefaultFromConfig() { final int userId = 0; ComponentName oldDefaultComponent = ComponentName.unflattenFromString("package/Component1"); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 60c4ac777906..e70ed5f256bf 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -33,6 +33,8 @@ import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON; import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_API; import static android.service.notification.ZenModeConfig.ZEN_TAG; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; @@ -524,7 +526,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.zenMode = INTERRUPTION_FILTER; rule.modified = true; rule.name = NAME; - rule.snoozing = true; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); rule.zenPolicy = POLICY; @@ -546,7 +548,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule parceled = new ZenModeConfig.ZenRule(parcel); assertEquals(rule.pkg, parceled.pkg); - assertEquals(rule.snoozing, parceled.snoozing); + assertEquals(rule.getConditionOverride(), parceled.getConditionOverride()); assertEquals(rule.enabler, parceled.enabler); assertEquals(rule.component, parceled.component); assertEquals(rule.configurationActivity, parceled.configurationActivity); @@ -625,7 +627,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.zenMode = INTERRUPTION_FILTER; rule.modified = true; rule.name = NAME; - rule.snoozing = true; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); rule.zenPolicy = POLICY; rule.zenDeviceEffects = new ZenDeviceEffects.Builder() @@ -662,7 +664,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.pkg, fromXml.pkg); // always resets on reboot - assertFalse(fromXml.snoozing); + assertEquals(OVERRIDE_NONE, fromXml.getConditionOverride()); //should all match original assertEquals(rule.component, fromXml.component); assertEquals(rule.configurationActivity, fromXml.configurationActivity); @@ -1115,7 +1117,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; rule.modified = true; rule.name = "name"; - rule.snoozing = false; rule.pkg = "b"; config.automaticRules.put("key", rule); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 9af00218dda4..91eb2edeee13 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -158,7 +158,10 @@ public class ZenModeDiffTest extends UiServiceTestCase { RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS)); } - if (!(Flags.modesApi() && Flags.modesUi())) { + if (Flags.modesApi() && Flags.modesUi()) { + exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete. + } else { + exemptFields.add(RuleDiff.FIELD_CONDITION_OVERRIDE); exemptFields.add(RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS); } return exemptFields; @@ -339,7 +342,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; rule.modified = false; rule.name = "name"; - rule.snoozing = true; + rule.setConditionOverride(ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE); rule.pkg = "a"; if (android.app.Flags.modesApi()) { rule.allowManualInvocation = true; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 776a840466c8..212e61e10448 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -51,6 +51,7 @@ import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; +import static android.service.notification.Condition.SOURCE_CONTEXT; import static android.service.notification.Condition.SOURCE_SCHEDULE; import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_FALSE; @@ -61,6 +62,9 @@ import static android.service.notification.ZenModeConfig.ORIGIN_INIT_USER; import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; @@ -2999,11 +3003,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertWithMessage("Failure for origin " + origin.name()) .that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertWithMessage("Failure for origin " + origin.name()) - .that(mZenModeHelper.mConfig.automaticRules.get(activeRuleId).snoozing) - .isTrue(); + .that(mZenModeHelper.mConfig.automaticRules + .get(activeRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_DEACTIVATE); assertWithMessage("Failure for origin " + origin.name()) - .that(mZenModeHelper.mConfig.automaticRules.get(inactiveRuleId).snoozing) - .isFalse(); + .that(mZenModeHelper.mConfig.automaticRules + .get(inactiveRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); } } @@ -3038,16 +3044,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertWithMessage("Failure for origin " + origin.name()).that( mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(activeRuleId).snoozing).isFalse(); + config.automaticRules.get(activeRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + config.automaticRules.get(inactiveRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); } else { assertWithMessage("Failure for origin " + origin.name()).that( mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(activeRuleId).snoozing).isTrue(); + config.automaticRules.get(activeRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_DEACTIVATE); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + config.automaticRules.get(inactiveRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); } } } @@ -4288,7 +4298,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.zenMode = INTERRUPTION_FILTER_ZR; rule.modified = true; rule.name = NAME; - rule.snoozing = true; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); rule.zenPolicy = POLICY; @@ -5000,7 +5010,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, "", SYSTEM_UID); - assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing); + assertEquals(OVERRIDE_NONE, + mZenModeHelper.mConfig.automaticRules.get(createdId).getConditionOverride()); } @Test @@ -5713,7 +5724,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(storedRule.isAutomaticActive()).isFalse(); assertThat(storedRule.isTrueOrUnknown()).isFalse(); assertThat(storedRule.condition).isNull(); - assertThat(storedRule.snoozing).isFalse(); + assertThat(storedRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); } @@ -6039,15 +6050,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); - assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0); - assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) + .isEqualTo(OVERRIDE_DEACTIVATE); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_ALARMS); - assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) .isEqualTo(STATE_TRUE); } @@ -6451,6 +6465,160 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_UNKNOWN); } + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualActivation_appliesOverride() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + + ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + assertThat(zenRule.condition).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualActivationAndThenDeactivation_removesOverride() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + ZenRule zenRule; + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + assertThat(zenRule.condition).isNull(); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + assertThat(zenRule.condition).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualDeactivation_appliesOverride() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, SYSTEM_UID); + ZenRule zenRuleOn = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRuleOn.isAutomaticActive()).isTrue(); + assertThat(zenRuleOn.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + assertThat(zenRuleOn.condition).isNotNull(); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + ZenRule zenRuleOff = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRuleOff.isAutomaticActive()).isFalse(); + assertThat(zenRuleOff.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); + assertThat(zenRuleOff.condition).isNotNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_ifManualActive_appCannotDeactivateBeforeActivating() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + ZenRule zenRule; + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_ifManualInactive_appCannotReactivateBeforeDeactivating() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + ZenRule zenRule; + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java index 72ef888aa061..6f06050f55ff 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java @@ -119,6 +119,7 @@ public class VibrationSettingsTest { USAGE_PHYSICAL_EMULATION, USAGE_RINGTONE, USAGE_TOUCH, + USAGE_IME_FEEDBACK }; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -525,7 +526,7 @@ public class VibrationSettingsTest { setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); for (int usage : ALL_USAGES) { - if (usage == USAGE_TOUCH) { + if (usage == USAGE_TOUCH || usage == USAGE_IME_FEEDBACK) { assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_FOR_SETTINGS); } else { assertVibrationNotIgnoredForUsage(usage); @@ -601,14 +602,14 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_withKeyboardSettingsOff_shouldIgnoreKeyboardVibration() { + setKeyboardVibrationSettingsSupported(true); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 0 /* OFF*/); - setKeyboardVibrationSettingsSupported(true); // Keyboard touch ignored. assertVibrationIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build(), Vibration.Status.IGNORED_FOR_SETTINGS); @@ -617,7 +618,7 @@ public class VibrationSettingsTest { assertVibrationNotIgnoredForUsage(USAGE_TOUCH); assertVibrationNotIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF) .build()); @@ -625,9 +626,9 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_withKeyboardSettingsOn_shouldNotIgnoreKeyboardVibration() { + setKeyboardVibrationSettingsSupported(true); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */); - setKeyboardVibrationSettingsSupported(true); // General touch ignored. assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS); @@ -635,16 +636,16 @@ public class VibrationSettingsTest { // Keyboard touch not ignored. assertVibrationNotIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build()); } @Test - public void shouldIgnoreVibration_notSupportKeyboardVibration_ignoresKeyboardTouchVibration() { + public void shouldIgnoreVibration_notSupportKeyboardVibration_followsTouchFeedbackSettings() { + setKeyboardVibrationSettingsSupported(false); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */); - setKeyboardVibrationSettingsSupported(false); // General touch ignored. assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS); @@ -652,7 +653,7 @@ public class VibrationSettingsTest { // Keyboard touch ignored. assertVibrationIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build(), Vibration.Status.IGNORED_FOR_SETTINGS); diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 771e290f60fd..e57e36d36621 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -59,6 +59,7 @@ public class DimmerTests extends WindowTestsBase { TestWindowContainer(WindowManagerService wm) { super(wm); + setVisibleRequested(true); } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java index ffaa2d820203..400fe8b05526 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -45,6 +46,12 @@ import org.mockito.invocation.InvocationOnMock; import java.util.function.Supplier; +/** + * Test class for {@link Letterbox}. + * <p> + * Build/Install/Run: + * atest WmTests:LetterboxTest + */ @SmallTest @Presubmit public class LetterboxTest { @@ -53,21 +60,21 @@ public class LetterboxTest { SurfaceControlMocker mSurfaces; SurfaceControl.Transaction mTransaction; - private boolean mAreCornersRounded = false; - private int mColor = Color.BLACK; - private boolean mHasWallpaperBackground = false; - private int mBlurRadius = 0; - private float mDarkScrimAlpha = 0.5f; private SurfaceControl mParentSurface = mock(SurfaceControl.class); + private AppCompatLetterboxOverrides mLetterboxOverrides; @Before public void setUp() throws Exception { mSurfaces = new SurfaceControlMocker(); + mLetterboxOverrides = mock(AppCompatLetterboxOverrides.class); + doReturn(false).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners(); + doReturn(Color.valueOf(Color.BLACK)).when(mLetterboxOverrides) + .getLetterboxBackgroundColor(); + doReturn(false).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox(); + doReturn(0).when(mLetterboxOverrides).getLetterboxWallpaperBlurRadiusPx(); + doReturn(0.5f).when(mLetterboxOverrides).getLetterboxWallpaperDarkScrimAlpha(); mLetterbox = new Letterbox(mSurfaces, StubTransaction::new, - () -> mAreCornersRounded, () -> Color.valueOf(mColor), - () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha, - mock(AppCompatReachabilityPolicy.class), - () -> mParentSurface); + mock(AppCompatReachabilityPolicy.class), mLetterboxOverrides, () -> mParentSurface); mTransaction = spy(StubTransaction.class); } @@ -183,7 +190,8 @@ public class LetterboxTest { verify(mTransaction).setColor(mSurfaces.top, new float[]{0, 0, 0}); - mColor = Color.GREEN; + doReturn(Color.valueOf(Color.GREEN)).when(mLetterboxOverrides) + .getLetterboxBackgroundColor(); assertTrue(mLetterbox.needsApplySurfaceChanges()); @@ -200,12 +208,12 @@ public class LetterboxTest { verify(mTransaction).setAlpha(mSurfaces.top, 1.0f); assertFalse(mLetterbox.needsApplySurfaceChanges()); - mHasWallpaperBackground = true; + doReturn(true).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox(); assertTrue(mLetterbox.needsApplySurfaceChanges()); applySurfaceChanges(); - verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, mDarkScrimAlpha); + verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, /* alpha */ 0.5f); } @Test @@ -234,7 +242,7 @@ public class LetterboxTest { @Test public void testApplySurfaceChanges_cornersRounded_surfaceFullWindowSurfaceCreated() { - mAreCornersRounded = true; + doReturn(true).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners(); mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000)); applySurfaceChanges(); @@ -243,7 +251,7 @@ public class LetterboxTest { @Test public void testApplySurfaceChanges_wallpaperBackground_surfaceFullWindowSurfaceCreated() { - mHasWallpaperBackground = true; + doReturn(true).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox(); mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000)); applySurfaceChanges(); @@ -252,7 +260,7 @@ public class LetterboxTest { @Test public void testNotIntersectsOrFullyContains_cornersRounded() { - mAreCornersRounded = true; + doReturn(true).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners(); mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(0, 0)); applySurfaceChanges(); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 695068a5842a..04997f8da86a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -123,7 +123,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { // Do not apply crop if taskbar is collapsed taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS); - assertNull(mController.getExpandedTaskbarOrNull(mainWindow)); + assertNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow)); mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4, SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4); @@ -145,7 +145,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { // Apply crop if taskbar is expanded taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS); - assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow)); + assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow)); mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT); @@ -169,7 +169,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { // Apply crop if taskbar is expanded taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS); - assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow)); + assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow)); // With SizeCompat scaling doReturn(true).when(mActivity).inSizeCompatMode(); mainWindow.mInvGlobalScale = scaling; diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index aa997ac42c66..31488084daa3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -247,9 +247,9 @@ public class SizeCompatTests extends WindowTestsBase { .build(); mTask.addChild(translucentActivity); - spyOn(translucentActivity.mLetterboxUiController); - doReturn(true).when(translucentActivity.mLetterboxUiController) - .shouldShowLetterboxUi(any()); + spyOn(translucentActivity.mAppCompatController.getAppCompatLetterboxPolicy()); + doReturn(true).when(translucentActivity.mAppCompatController + .getAppCompatLetterboxPolicy()).shouldShowLetterboxUi(any()); addWindowToActivity(translucentActivity); translucentActivity.mRootWindowContainer.performSurfacePlacement(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 2b611b754edd..18255b8d82f8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1488,7 +1488,7 @@ public class WindowOrganizerTests extends WindowTestsBase { @Test public void testReorderWithParents() { /* - root + default TDA ____|______ | | firstTda secondTda @@ -1496,10 +1496,12 @@ public class WindowOrganizerTests extends WindowTestsBase { firstRootTask secondRootTask */ - final TaskDisplayArea firstTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea(); - final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( - mDisplayContent, mRootWindowContainer.mWmService, "TestTaskDisplayArea", + final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea", FEATURE_VENDOR_FIRST); + final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea", + FEATURE_VENDOR_FIRST + 1); final Task firstRootTask = firstTaskDisplayArea.createRootTask( WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */); final Task secondRootTask = secondTaskDisplayArea.createRootTask( @@ -1508,9 +1510,6 @@ public class WindowOrganizerTests extends WindowTestsBase { .setTask(firstRootTask).build(); final ActivityRecord secondActivity = new ActivityBuilder(mAtm) .setTask(secondRootTask).build(); - // This assertion is just a defense to ensure that firstRootTask is not the top most - // by default - assertThat(mDisplayContent.getTopRootTask()).isEqualTo(secondRootTask); WindowContainerTransaction wct = new WindowContainerTransaction(); // Reorder to top @@ -1533,6 +1532,67 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + public void testReorderDisplayArea() { + /* + defaultTda + ____|______ + | | + firstTda secondTda + | | + firstRootTask secondRootTask + + */ + final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea", + FEATURE_VENDOR_FIRST); + final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea", + FEATURE_VENDOR_FIRST + 1); + final Task firstRootTask = firstTaskDisplayArea.createRootTask( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */); + final Task secondRootTask = secondTaskDisplayArea.createRootTask( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */); + final ActivityRecord firstActivity = new ActivityBuilder(mAtm) + .setTask(firstRootTask).build(); + final ActivityRecord secondActivity = new ActivityBuilder(mAtm) + .setTask(secondRootTask).build(); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Reorder to top + wct.reorder(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), true /* onTop */, + true /* includingParents */); + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); + assertThat(mDisplayContent.getTopRootTask()).isEqualTo(firstRootTask); + + // Reorder to bottom + wct.reorder(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), false /* onTop */, + true /* includingParents */); + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); + assertThat(mDisplayContent.getBottomMostTask()).isEqualTo(firstRootTask); + } + + @Test + public void testReparentDisplayAreaUnsupported() { + final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea", + FEATURE_VENDOR_FIRST); + final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea", + FEATURE_VENDOR_FIRST + 1); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparent(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), + secondTaskDisplayArea.mRemoteToken.toWindowContainerToken(), + true /* onTop */ + ); + + assertThrows(UnsupportedOperationException.class, () -> + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct) + ); + } + + @Test public void testAppearDeferThenVanish() { final ITaskOrganizer organizer = registerMockOrganizer(); final Task rootTask = createRootTask(); diff --git a/telephony/java/android/telephony/PcoData.java b/telephony/java/android/telephony/PcoData.java index 39e4f2f799d8..3cc32c657fd7 100644 --- a/telephony/java/android/telephony/PcoData.java +++ b/telephony/java/android/telephony/PcoData.java @@ -19,6 +19,8 @@ package android.telephony; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.telephony.uicc.IccUtils; + import java.util.Arrays; import java.util.Objects; @@ -84,8 +86,8 @@ public class PcoData implements Parcelable { @Override public String toString() { - return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + ", contents[" + - contents.length + "])"; + return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + " " + + IccUtils.bytesToHexString(contents) + ")"; } @Override diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java index db167c0592df..127bbff01575 100644 --- a/telephony/java/android/telephony/ServiceState.java +++ b/telephony/java/android/telephony/ServiceState.java @@ -1211,6 +1211,8 @@ public class ServiceState implements Parcelable { .append(", mIsDataRoamingFromRegistration=") .append(mIsDataRoamingFromRegistration) .append(", mIsIwlanPreferred=").append(mIsIwlanPreferred) + .append(", mIsUsingNonTerrestrialNetwork=") + .append(isUsingNonTerrestrialNetwork()) .append("}").toString(); } } diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index c132792d374b..6c3eae11eab9 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -244,6 +244,10 @@ class Context : public IAaptContext { return verbose_; } + void SetVerbose(bool verbose) { + verbose_ = verbose; + } + int GetMinSdkVersion() override { return min_sdk_; } @@ -388,6 +392,8 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { } Context context; + context.SetVerbose(verbose_); + StringPiece path = args[0]; unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(path, context.GetDiagnostics()); if (apk == nullptr) { diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp new file mode 100644 index 000000000000..2cebfe9790d0 --- /dev/null +++ b/tools/systemfeatures/Android.bp @@ -0,0 +1,63 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "systemfeatures-gen-lib", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "guava", + "javapoet", + ], +} + +java_binary_host { + name: "systemfeatures-gen-tool", + main_class: "com.android.systemfeatures.SystemFeaturesGenerator", + static_libs: ["systemfeatures-gen-lib"], +} + +// TODO(b/203143243): Add golden diff test for generated sources. +// Functional runtime behavior is covered in systemfeatures-gen-tests. +genrule { + name: "systemfeatures-gen-tests-srcs", + cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true > $(location RoNoFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RoFeatures.java)", + out: [ + "RwNoFeatures.java", + "RoNoFeatures.java", + "RwFeatures.java", + "RoFeatures.java", + ], + tools: ["systemfeatures-gen-tool"], +} + +java_test_host { + name: "systemfeatures-gen-tests", + test_suites: ["general-tests"], + srcs: [ + "tests/**/*.java", + ":systemfeatures-gen-tests-srcs", + ], + test_options: { + unit_test: true, + }, + static_libs: [ + "aconfig-annotations-lib", + "framework-annotations-lib", + "junit", + "objenesis", + "mockito", + "truth", + ], +} diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt new file mode 100644 index 000000000000..9bfda451067f --- /dev/null +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemfeatures + +import com.google.common.base.CaseFormat +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.TypeSpec +import javax.lang.model.element.Modifier + +/* + * Simple Java code generator that takes as input a list of defined features and generates an + * accessory class based on the provided versions. + * + * <p>Example: + * + * <pre> + * <cmd> com.foo.RoSystemFeatures --readonly=true \ + * --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348 + * </pre> + * + * This generates a class that has the following signature: + * + * <pre> + * package com.foo; + * public final class RoSystemFeatures { + * @AssumeTrueForR8 + * public static boolean hasFeatureWatch(Context context); + * @AssumeFalseForR8 + * public static boolean hasFeatureAutomotive(Context context); + * @AssumeTrueForR8 + * public static boolean hasFeatureVulkan(Context context); + * public static Boolean maybeHasFeature(String feature, int version); + * } + * </pre> + */ +object SystemFeaturesGenerator { + private const val FEATURE_ARG = "--feature=" + private const val READONLY_ARG = "--readonly=" + private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager") + private val CONTEXT_CLASS = ClassName.get("android.content", "Context") + private val ASSUME_TRUE_CLASS = + ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8") + private val ASSUME_FALSE_CLASS = + ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8") + + private fun usage() { + println("Usage: SystemFeaturesGenerator <outputClassName> [options]") + println(" Options:") + println(" --readonly=true|false Whether to encode features as build-time constants") + println(" --feature=\$NAME:\$VER A feature+version pair (blank version == disabled)") + } + + /** Main entrypoint for build-time system feature codegen. */ + @JvmStatic + fun main(args: Array<String>) { + if (args.size < 1) { + usage() + return + } + + var readonly = false + var outputClassName: ClassName? = null + val features = mutableListOf<FeatureInfo>() + for (arg in args) { + when { + arg.startsWith(READONLY_ARG) -> + readonly = arg.substring(READONLY_ARG.length).toBoolean() + arg.startsWith(FEATURE_ARG) -> { + features.add(parseFeatureArg(arg)) + } + else -> outputClassName = ClassName.bestGuess(arg) + } + } + + outputClassName + ?: run { + println("Output class name must be provided.") + usage() + return + } + + val classBuilder = + TypeSpec.classBuilder(outputClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("@hide") + + addFeatureMethodsToClass(classBuilder, readonly, features) + addMaybeFeatureMethodToClass(classBuilder, readonly, features) + + // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency. + JavaFile.builder(outputClassName.packageName(), classBuilder.build()) + .build() + .writeTo(System.out) + } + + /* + * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional. + * * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled) + * * "--feature=WATCH:7" -> Feature enabled w/ version 7 + * * "--feature=WATCH:" -> Feature disabled + */ + private fun parseFeatureArg(arg: String): FeatureInfo { + val featureArgs = arg.substring(FEATURE_ARG.length).split(":") + val name = featureArgs[0].let { if (!it.startsWith("FEATURE_")) "FEATURE_$it" else it } + val version = featureArgs.getOrNull(1)?.toIntOrNull() + return FeatureInfo(name, version) + } + + /* + * Adds per-feature query methods to the class with the form: + * {@code public static boolean hasFeatureX(Context context)}, + * returning the fallback value from PackageManager if not readonly. + */ + private fun addFeatureMethodsToClass( + builder: TypeSpec.Builder, + readonly: Boolean, + features: List<FeatureInfo> + ) { + for (feature in features) { + // Turn "FEATURE_FOO" into "hasFeatureFoo". + val methodName = + "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name) + val methodBuilder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(Boolean::class.java) + .addParameter(CONTEXT_CLASS, "context") + + if (readonly) { + val featureEnabled = compareValues(feature.version, 0) >= 0 + methodBuilder.addAnnotation( + if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS + ) + methodBuilder.addStatement("return $featureEnabled") + } else { + methodBuilder.addStatement( + "return hasFeatureFallback(context, \$T.\$N)", + PACKAGEMANAGER_CLASS, + feature.name + ) + } + builder.addMethod(methodBuilder.build()) + } + + if (!readonly) { + builder.addMethod( + MethodSpec.methodBuilder("hasFeatureFallback") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(Boolean::class.java) + .addParameter(CONTEXT_CLASS, "context") + .addParameter(String::class.java, "featureName") + .addStatement( + "return context.getPackageManager().hasSystemFeature(featureName, 0)" + ) + .build() + ) + } + } + + /* + * Adds a generic query method to the class with the form: {@code public static boolean + * maybeHasFeature(String featureName, int version)}, returning null if the feature version is + * undefined or not readonly. + * + * This method is useful for internal usage within the framework, e.g., from the implementation + * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only + * want a valid result if it's defined as readonly, and we want a custom fallback otherwise + * (e.g., to the existing runtime binder query). + */ + private fun addMaybeFeatureMethodToClass( + builder: TypeSpec.Builder, + readonly: Boolean, + features: List<FeatureInfo> + ) { + val methodBuilder = + MethodSpec.methodBuilder("maybeHasFeature") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addAnnotation(ClassName.get("android.annotation", "Nullable")) + .returns(Boolean::class.javaObjectType) // Use object type for nullability + .addParameter(String::class.java, "featureName") + .addParameter(Int::class.java, "version") + + if (readonly) { + methodBuilder.beginControlFlow("switch (featureName)") + for (feature in features) { + methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name) + if (feature.version != null) { + methodBuilder.addStatement("return \$L >= version", feature.version) + } else { + methodBuilder.addStatement("return false") + } + } + methodBuilder.addCode("default: ") + methodBuilder.addStatement("break") + methodBuilder.endControlFlow() + } + methodBuilder.addStatement("return null") + builder.addMethod(methodBuilder.build()) + } + + private data class FeatureInfo(val name: String, val version: Int?) +} diff --git a/tools/systemfeatures/tests/Context.java b/tools/systemfeatures/tests/Context.java new file mode 100644 index 000000000000..630bc0771a01 --- /dev/null +++ b/tools/systemfeatures/tests/Context.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.content.pm.PackageManager; + +/** Stub for testing. */ +public class Context { + /** @hide */ + public PackageManager getPackageManager() { + return null; + } +} diff --git a/tools/systemfeatures/tests/PackageManager.java b/tools/systemfeatures/tests/PackageManager.java new file mode 100644 index 000000000000..645d500bc762 --- /dev/null +++ b/tools/systemfeatures/tests/PackageManager.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +/** Stub for testing */ +public class PackageManager { + public static final String FEATURE_AUTO = "automotive"; + public static final String FEATURE_VULKAN = "vulkan"; + public static final String FEATURE_WATCH = "watch"; + public static final String FEATURE_WIFI = "wifi"; + + /** @hide */ + public boolean hasSystemFeature(String featureName, int version) { + return false; + } +} diff --git a/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java new file mode 100644 index 000000000000..547d2cbd26f9 --- /dev/null +++ b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemfeatures; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; + +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; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class SystemFeaturesGeneratorTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + + @Before + public void setUp() { + when(mContext.getPackageManager()).thenReturn(mPackageManager); + } + + @Test + public void testReadonlyDisabledNoDefinedFeatures() { + // Always report null for conditional queries if readonly codegen is disabled. + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyNoDefinedFeatures() { + // If no features are explicitly declared as readonly available, always report + // null for conditional queries. + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyDisabledWithDefinedFeatures() { + // Always fall back to the PackageManager for defined, explicit features queries. + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true); + assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse(); + + // For defined and undefined features, conditional queries should report null (unknown). + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyWithDefinedFeatures() { + // Always use the build-time feature version for defined, explicit feature queries, never + // falling back to the runtime query. + assertThat(RoFeatures.hasFeatureWatch(mContext)).isTrue(); + assertThat(RoFeatures.hasFeatureWifi(mContext)).isTrue(); + assertThat(RoFeatures.hasFeatureVulkan(mContext)).isFalse(); + assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); + verify(mPackageManager, never()).hasSystemFeature(anyString(), anyInt()); + + // For defined feature types, conditional queries should reflect the build-time versions. + // VERSION=1 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 100)).isFalse(); + + // VERSION=0 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 100)).isFalse(); + + // VERSION=-1 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); + + // DISABLED + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse(); + + // For undefined types, conditional queries should report null (unknown). + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull(); + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull(); + } +} |