Merge "Make testing windows visible" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 4e34b63..59a7cbc 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -21,6 +21,7 @@
// !!! 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 @@
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 856dba3..9eea712 100644
--- a/apct-tests/perftests/multiuser/Android.bp
+++ b/apct-tests/perftests/multiuser/Android.bp
@@ -45,3 +45,8 @@
"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 ea039a7..36c91758 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10684,6 +10684,7 @@
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 89efa9b..d318812 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -961,6 +961,17 @@
@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 e73f471..114a2c4 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.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 @@
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 0000000..a01e373
--- /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 0000000..018bc75
--- /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 0000000..367effc
--- /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 9aebfc8..9dccc9a 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.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 @@
/**
* 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 b0ab11f..1fab3cf 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 59fca3b..273519b 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -288,6 +288,13 @@
}
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 9007b62..b11961c 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -638,17 +638,17 @@
* 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 901f6b7..df5d864 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -57,7 +57,8 @@
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 @@
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 @@
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 @@
return true;
} else if (mShowEmergencyCallButton) {
return true;
- } else if (mComponentNameForConfirmDeviceCredentialActivity != null) {
+ } else if (mRealCallerForConfirmDeviceCredentialActivity != null) {
return true;
}
return false;
@@ -321,10 +324,8 @@
mShowEmergencyCallButton = showEmergencyCallButton;
}
- public void setComponentNameForConfirmDeviceCredentialActivity(
- ComponentName componentNameForConfirmDeviceCredentialActivity) {
- mComponentNameForConfirmDeviceCredentialActivity =
- componentNameForConfirmDeviceCredentialActivity;
+ public void setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) {
+ mRealCallerForConfirmDeviceCredentialActivity = realCaller;
}
public void setUseParentProfileForDeviceCredential(
@@ -332,6 +333,14 @@
mUseParentProfileForDeviceCredential = useParentProfileForDeviceCredential;
}
+ /**
+ * Set the class name of ConfirmDeviceCredentialActivity.
+ */
+ void setClassNameIfItIsConfirmDeviceCredentialActivity(String className) {
+ mClassNameIfItIsConfirmDeviceCredentialActivity = className;
+ }
+
+
// Getters
/**
@@ -455,8 +464,15 @@
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/os/Trace.java b/core/java/android/os/Trace.java
index edb3a64..4a37e0a 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -520,8 +520,20 @@
* @param counterValue The counter value.
*/
public static void setCounter(@NonNull String counterName, long counterValue) {
- if (isTagEnabled(TRACE_TAG_APP)) {
- nativeTraceCounter(TRACE_TAG_APP, counterName, counterValue);
+ setCounter(TRACE_TAG_APP, counterName, counterValue);
+ }
+
+ /**
+ * Writes trace message to indicate the value of a given counter under a given trace tag.
+ *
+ * @param traceTag The trace tag.
+ * @param counterName The counter name to appear in the trace.
+ * @param counterValue The counter value.
+ * @hide
+ */
+ public static void setCounter(long traceTag, @NonNull String counterName, long counterValue) {
+ if (isTagEnabled(traceTag)) {
+ nativeTraceCounter(traceTag, counterName, counterValue);
}
}
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index 3a254c1..bdda42a 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -19,6 +19,8 @@
import android.os.Environment;
import android.os.UserHandle;
+import com.android.internal.util.ArrayUtils;
+
import java.io.File;
/**
@@ -45,7 +47,7 @@
}
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 224379b..fc6c2e8 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_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.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 @@
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 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 @@
// 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 @@
if (Flags.modesUi()) {
disabledOrigin = source.readInt();
legacySuppressedEffects = source.readInt();
+ conditionOverride = source.readInt();
}
}
}
@@ -2698,6 +2732,7 @@
if (Flags.modesUi()) {
dest.writeInt(disabledOrigin);
dest.writeInt(legacySuppressedEffects);
+ dest.writeInt(conditionOverride);
}
}
}
@@ -2708,9 +2743,16 @@
.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 @@
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 @@
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 @@
if (Flags.modesUi()) {
finalEquals = finalEquals
&& other.disabledOrigin == disabledOrigin
- && other.legacySuppressedEffects == legacySuppressedEffects;
+ && other.legacySuppressedEffects == legacySuppressedEffects
+ && other.conditionOverride == conditionOverride;
}
}
@@ -2832,7 +2888,8 @@
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 @@
}
}
+ // 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 a37e227..05c2a9c 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -454,6 +454,8 @@
*/
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 @@
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 09306c7..288be9c 100644
--- a/core/java/android/view/animation/Animation.java
+++ b/core/java/android/view/animation/Animation.java
@@ -28,6 +28,7 @@
import android.os.SystemProperties;
import android.util.AttributeSet;
import android.util.TypedValue;
+import android.view.WindowInsets;
import dalvik.system.CloseGuard;
@@ -881,12 +882,13 @@
}
/**
- * @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 5aaa994..bbdc9d0 100644
--- a/core/java/android/view/animation/AnimationSet.java
+++ b/core/java/android/view/animation/AnimationSet.java
@@ -21,6 +21,7 @@
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 @@
/** @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 210eb8a..1aeee07 100644
--- a/core/java/android/view/animation/ExtendAnimation.java
+++ b/core/java/android/view/animation/ExtendAnimation.java
@@ -20,6 +20,7 @@
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 @@
/** @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 a6ae948..adbc598 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -61,16 +61,6 @@
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 @@
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 69d1cb3..7bfb800 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -210,8 +210,16 @@
*/
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 @@
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 @@
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 @@
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 12804d4..e7f0560 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -163,7 +163,7 @@
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 25e7107..26d180c 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 @@
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 @@
@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 d0e2c99..ee3e6f3 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.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 @@
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 475475b..90eeb58 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 @@
@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/Android.bp b/libs/WindowManager/Shell/Android.bp
index 5135e9e..1c3d9c3 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -166,6 +166,16 @@
},
}
+java_library {
+ name: "WindowManager-Shell-lite-proto",
+
+ srcs: ["src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto"],
+
+ proto: {
+ type: "lite",
+ },
+}
+
filegroup {
name: "wm_shell-shared-aidls",
@@ -215,6 +225,7 @@
"androidx.core_core-animation",
"androidx.core_core-ktx",
"androidx.arch.core_core-runtime",
+ "androidx.datastore_datastore",
"androidx.compose.material3_material3",
"androidx-constraintlayout_constraintlayout",
"androidx.dynamicanimation_dynamicanimation",
@@ -225,6 +236,7 @@
"//frameworks/libs/systemui:iconloader_base",
"com_android_wm_shell_flags_lib",
"WindowManager-Shell-proto",
+ "WindowManager-Shell-lite-proto",
"WindowManager-Shell-shared",
"perfetto_trace_java_protos",
"dagger2",
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 5e67333..84f7bb2 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.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 @@
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 bc59a23..debcba0 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 df5f1e4..6a62d7a3 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 8d30db6..5355138 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 @@
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 @@
/** 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 @@
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 @@
@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 5696a54..d2cef4b 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 @@
// 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 @@
@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 7275c64..1242287 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 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 @@
}
if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION
- && !isGestureBackTransition(info)) {
+ && isNotGestureBackTransition(info)) {
return false;
}
@@ -1364,7 +1365,7 @@
// 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 @@
// 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 @@
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 @@
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 @@
@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 @@
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 @@
}
}
+ 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 f9a1d94..dc511be 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 @@
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 7dbbb04..5cd2cb7 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.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 @@
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 @@
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 @@
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 @@
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 949a723..29520ef 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 @@
}
/**
+ * 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 @@
* @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 @@
}
@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 @@
}
@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 b6da761..3c6c6fa 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 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 @@
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 @@
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 fdb4523..a0c0a25 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 @@
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 @@
/* 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 3d9bf03..4e80e90 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 @@
fun isStackExpanded(): Boolean
fun isShowingAsBubbleBar(): Boolean
fun hideCurrentInputMethod()
+ fun updateBubbleBarLocation(location: BubbleBarLocation)
companion object {
/**
@@ -78,6 +81,10 @@
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 efa1031..f002d89 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 @@
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 5e2141a..5f8f0fd 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 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 @@
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 @@
/* 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 589dfd2..9a27fb6 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 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 @@
/**
* 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 0907ddd..5c78974 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 @@
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 @@
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 b7834db..6d868d2 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.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.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.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 @@
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 @@
// 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 @@
/** 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 @@
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/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b8b62a7..e787a3d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -72,6 +72,7 @@
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.GlobalDragListener;
import com.android.wm.shell.freeform.FreeformComponents;
@@ -679,6 +680,13 @@
return new DesktopModeEventLogger();
}
+ @WMSingleton
+ @Provides
+ static AppHandleEducationDatastoreRepository provideAppHandleEducationDatastoreRepository(
+ Context context) {
+ return new AppHandleEducationDatastoreRepository(context);
+ }
+
//
// Drag and drop
//
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 400882a..05c9d02 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 @@
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 @@
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 @@
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 73aa7ce..a6ed3b8 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.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.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 @@
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 @@
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 38675129..597637d 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 @@
// 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 @@
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 @@
return
}
+ taskToMinimize.transitionInfo = info
+ activeTransitionTokensAndTasks[transition] = taskToMinimize
this@DesktopTasksLimiter.markTaskMinimized(
taskToMinimize.displayId, taskToMinimize.taskId)
}
@@ -121,7 +121,7 @@
}
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 @@
}
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 @@
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/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
new file mode 100644
index 0000000..bf4a2ab
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.wm.shell.desktopmode.education.data
+
+import android.content.Context
+import android.util.Log
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.Serializer
+import androidx.datastore.dataStore
+import androidx.datastore.dataStoreFile
+import com.android.framework.protobuf.InvalidProtocolBufferException
+import com.android.internal.annotations.VisibleForTesting
+import java.io.InputStream
+import java.io.OutputStream
+import kotlinx.coroutines.flow.first
+
+/**
+ * Manages interactions with the App Handle education datastore.
+ *
+ * This class provides a layer of abstraction between the UI/business logic and the underlying
+ * DataStore.
+ */
+class AppHandleEducationDatastoreRepository
+@VisibleForTesting
+constructor(private val dataStore: DataStore<WindowingEducationProto>) {
+ constructor(
+ context: Context
+ ) : this(
+ DataStoreFactory.create(
+ serializer = WindowingEducationProtoSerializer,
+ produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }))
+
+ /**
+ * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the
+ * DataStore is empty or there's an error reading, it returns the default value of Proto.
+ */
+ suspend fun windowingEducationProto(): WindowingEducationProto =
+ try {
+ dataStore.data.first()
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to read from datastore")
+ WindowingEducationProto.getDefaultInstance()
+ }
+
+ companion object {
+ private const val TAG = "AppHandleEducationDatastoreRepository"
+ private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb"
+
+ object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> {
+
+ override val defaultValue: WindowingEducationProto =
+ WindowingEducationProto.getDefaultInstance()
+
+ override suspend fun readFrom(input: InputStream): WindowingEducationProto =
+ try {
+ WindowingEducationProto.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+
+ override suspend fun writeTo(windowingProto: WindowingEducationProto, output: OutputStream) =
+ windowingProto.writeTo(output)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
new file mode 100644
index 0000000..d29ec53
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+option java_package = "com.android.wm.shell.desktopmode.education.data";
+option java_multiple_files = true;
+
+// Desktop Windowing education data
+message WindowingEducationProto {
+ // Timestamp in milliseconds of when the education was last viewed.
+ optional int64 education_viewed_timestamp_millis = 1;
+ // Timestamp in milliseconds of when the feature was last used.
+ optional int64 feature_used_timestamp_millis = 2;
+ oneof education_data {
+ // Fields specific to app handle education
+ AppHandleEducation app_handle_education = 3;
+ }
+
+ message AppHandleEducation {
+ // Map that stores app launch count for corresponding package
+ map<string, int32> app_usage_stats = 1;
+ // Timestamp of when app_usage_stats was last cached
+ optional int64 app_usage_stats_last_update_timestamp_millis = 2;
+ }
+}
\ No newline at end of file
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 7784784..de6887a2 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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 e196254..1958825 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 @@
@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 a5e0550..3ffc9d7 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.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 @@
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/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 4465a16..acaf021 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.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 @@
@Presubmit
@Test
fun navBarLayerIsVisibleAtStartAndEnd() {
- Assume.assumeFalse(flicker.scenario.isTablet)
+ Assume.assumeFalse(usesTaskbar)
flicker.navBarLayerIsVisibleAtStartAndEnd()
}
@@ -54,7 +58,7 @@
@Presubmit
@Test
fun navBarLayerPositionAtStartAndEnd() {
- Assume.assumeFalse(flicker.scenario.isTablet)
+ Assume.assumeFalse(usesTaskbar)
flicker.navBarLayerPositionAtStartAndEnd()
}
@@ -66,7 +70,7 @@
@Presubmit
@Test
fun navBarWindowIsAlwaysVisible() {
- Assume.assumeFalse(flicker.scenario.isTablet)
+ Assume.assumeFalse(usesTaskbar)
flicker.navBarWindowIsAlwaysVisible()
}
@@ -76,7 +80,7 @@
@Presubmit
@Test
fun taskBarLayerIsVisibleAtStartAndEnd() {
- Assume.assumeTrue(flicker.scenario.isTablet)
+ Assume.assumeTrue(usesTaskbar)
flicker.taskBarLayerIsVisibleAtStartAndEnd()
}
@@ -88,7 +92,7 @@
@Presubmit
@Test
fun taskBarWindowIsAlwaysVisible() {
- Assume.assumeTrue(flicker.scenario.isTablet)
+ Assume.assumeTrue(usesTaskbar)
flicker.taskBarWindowIsAlwaysVisible()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index a040865..4d761e1 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -44,6 +44,8 @@
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "androidx.datastore_datastore",
+ "kotlinx_coroutines_test",
"androidx.dynamicanimation_dynamicanimation",
"dagger2",
"frameworks-base-testutils",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
new file mode 100644
index 0000000..4d40738
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.wm.shell.desktopmode.education
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
+import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@ExperimentalCoroutinesApi
+class AppHandleEducationDatastoreRepositoryTest {
+ private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
+ private lateinit var testDatastore: DataStore<WindowingEducationProto>
+ private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
+ private lateinit var datastoreScope: CoroutineScope
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+ datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+ testDatastore =
+ DataStoreFactory.create(
+ serializer =
+ AppHandleEducationDatastoreRepository.Companion.WindowingEducationProtoSerializer,
+ scope = datastoreScope) {
+ testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE)
+ }
+ datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore)
+ }
+
+ @After
+ fun tearDown() {
+ File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore")
+ .deleteRecursively()
+
+ datastoreScope.cancel()
+ }
+
+ @Test
+ fun getWindowingEducationProto_returnsCorrectProto() =
+ runTest(StandardTestDispatcher()) {
+ val windowingEducationProto =
+ createWindowingEducationProto(
+ educationViewedTimestampMillis = 123L,
+ featureUsedTimestampMillis = 124L,
+ appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
+ appUsageStatsLastUpdateTimestampMillis = 125L)
+ testDatastore.updateData { windowingEducationProto }
+
+ val resultProto = datastoreRepository.windowingEducationProto()
+
+ assertThat(resultProto).isEqualTo(windowingEducationProto)
+ }
+
+ private fun createWindowingEducationProto(
+ educationViewedTimestampMillis: Long? = null,
+ featureUsedTimestampMillis: Long? = null,
+ appUsageStats: Map<String, Int>? = null,
+ appUsageStatsLastUpdateTimestampMillis: Long? = null
+ ): WindowingEducationProto =
+ WindowingEducationProto.newBuilder()
+ .apply {
+ if (educationViewedTimestampMillis != null)
+ setEducationViewedTimestampMillis(educationViewedTimestampMillis)
+ if (featureUsedTimestampMillis != null)
+ setFeatureUsedTimestampMillis(featureUsedTimestampMillis)
+ setAppHandleEducation(
+ createAppHandleEducationProto(
+ appUsageStats, appUsageStatsLastUpdateTimestampMillis))
+ }
+ .build()
+
+ private fun createAppHandleEducationProto(
+ appUsageStats: Map<String, Int>? = null,
+ appUsageStatsLastUpdateTimestampMillis: Long? = null
+ ): WindowingEducationProto.AppHandleEducation =
+ WindowingEducationProto.AppHandleEducation.newBuilder()
+ .apply {
+ if (appUsageStats != null) putAllAppUsageStats(appUsageStats)
+ if (appUsageStatsLastUpdateTimestampMillis != null)
+ setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestampMillis)
+ }
+ .build()
+
+ companion object {
+ private const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+ private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb"
+ }
+}
diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp
index bedfc49..43b56c3 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 @@
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 b99a4ed..c4cd7c5 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 @@
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 @@
}
} // namespace android
-
-#endif // _ANDROID_BIG_BUFFER_H
diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp
index 382d21e..7e38f17 100644
--- a/libs/androidfw/tests/BigBuffer_test.cpp
+++ b/libs/androidfw/tests/BigBuffer_test.cpp
@@ -98,4 +98,20 @@
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 b3fb1e7..ff83610 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 492828d..64e503b32 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 @@
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 69c7410..6198d80 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 @@
* 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 @@
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 c88c4c9..0e71116 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.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 @@
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 @@
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 @@
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 @@
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 @@
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 7a66335..ebba7f1 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.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.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 @@
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 @@
btManager.profileManager.leAudioBroadcastProfile.onBroadcastStartedOrStopped
.map { isBroadcasting() }
.onStart { emit(isBroadcasting()) }
+ .onEach { logger.onAudioSharingStateChanged(it) }
.flowOn(backgroundCoroutineContext)
} else {
flowOf(false)
@@ -156,6 +160,7 @@
.map { getSecondaryGroupId() },
primaryGroupId.map { getSecondaryGroupId() })
.onStart { emit(getSecondaryGroupId()) }
+ .onEach { logger.onSecondaryGroupIdChanged(it) }
.flowOn(backgroundCoroutineContext)
.stateIn(
coroutineScope,
@@ -202,6 +207,7 @@
acc
}
}
+ .onEach { logger.onVolumeMapChanged(it) }
.flowOn(backgroundCoroutineContext)
} else {
emptyFlow()
@@ -220,6 +226,7 @@
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 0000000..84f7fcb
--- /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 0000000..18a4c6d
--- /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 078f0c8..8c5a085 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.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 @@
@Captor
private lateinit var assistantCallbackCaptor:
- ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback>
+ ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback>
@Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback>
@@ -110,6 +111,7 @@
@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 @@
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 @@
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 @@
Truth.assertThat(groupIds)
.containsExactly(
TEST_GROUP_ID_INVALID,
- TEST_GROUP_ID2)
+ TEST_GROUP_ID2
+ )
}
}
@@ -219,13 +236,16 @@
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 @@
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 @@
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 @@
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 @@
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 @@
Settings.Secure.putInt(
contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
- TEST_GROUP_ID1)
+ TEST_GROUP_ID1
+ )
assistantCallbackCaptor.value.sourceRemoved(device2)
}
@@ -334,7 +383,8 @@
Settings.Secure.putInt(
contentResolver,
BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
- TEST_GROUP_ID1)
+ TEST_GROUP_ID1
+ )
btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile)
}
@@ -343,12 +393,14 @@
.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 @@
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 389bf53..bd573fb 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 0000000..cc4cc8d
--- /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 00c7ae3..539519b 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 @@
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 9f3c2bf..1d9f469 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 2e036e6..6bea30f 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 @@
/** 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 d16617f..4ab771b 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 @@
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 e81d5d5..95e4b59 100644
--- a/packages/SystemUI/aconfig/biometrics_framework.aconfig
+++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig
@@ -3,9 +3,3 @@
# 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 e6fae7b..0314992 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -156,17 +156,6 @@
}
flag {
- name: "pss_app_selector_abrupt_exit_fix"
- namespace: "systemui"
- description: "Fixes the app selector abruptly disappearing without an animation, when the"
- "selected task is the foreground task."
- bug: "314385883"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "pss_app_selector_recents_split_screen"
namespace: "systemui"
description: "Allows recent apps selected for partial screenshare to be launched in split screen mode"
@@ -1248,6 +1237,16 @@
}
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"
@@ -1274,6 +1273,13 @@
}
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 0000000..04bcc36
--- /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/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
index 9da2a1b..5ffb6f8 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 @@
) {
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/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
new file mode 100644
index 0000000..5eabd22
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.SpringSpec
+import com.android.compose.animation.scene.content.state.ContentState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+internal fun CoroutineScope.animateContent(
+ transition: ContentState.Transition<*>,
+ oneOffAnimation: OneOffAnimation,
+ targetProgress: Float,
+ startTransition: () -> Unit,
+ finishTransition: () -> Unit,
+) {
+ // Start the transition. This will compute the TransformationSpec associated to [transition],
+ // which we need to initialize the Animatable that will actually animate it.
+ startTransition()
+
+ // The transition now contains the transformation spec that we should use to instantiate the
+ // Animatable.
+ val animationSpec = transition.transformationSpec.progressSpec
+ val visibilityThreshold =
+ (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
+ val replacedTransition = transition.replacedTransition
+ val initialProgress = replacedTransition?.progress ?: 0f
+ val initialVelocity = replacedTransition?.progressVelocity ?: 0f
+ val animatable =
+ Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
+ oneOffAnimation.animatable = it
+ }
+
+ // Animate the progress to its target value.
+ //
+ // Important: We start atomically to make sure that we start the coroutine even if it is
+ // cancelled right after it is launched, so that finishTransition() is correctly called.
+ // Otherwise, this transition will never be stopped and we will never settle to Idle.
+ oneOffAnimation.job =
+ launch(start = CoroutineStart.ATOMIC) {
+ try {
+ animatable.animateTo(targetProgress, animationSpec, initialVelocity)
+ } finally {
+ finishTransition()
+ }
+ }
+}
+
+internal class OneOffAnimation {
+ /**
+ * The animatable used to animate this transition.
+ *
+ * Note: This is lateinit because we need to first create this object so that
+ * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to
+ * the transition, which is needed to initialize this Animatable.
+ */
+ lateinit var animatable: Animatable<Float, AnimationVector1D>
+
+ /** The job that is animating [animatable]. */
+ lateinit var job: Job
+
+ val progress: Float
+ get() = animatable.value
+
+ val progressVelocity: Float
+ get() = animatable.velocity
+
+ fun finish(): Job = job
+}
+
+// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
+// and screen density.
+internal const val ProgressVisibilityThreshold = 1e-3f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 1fc1f98..68a6c98 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -16,15 +16,10 @@
package com.android.compose.animation.scene
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.SpringSpec
import com.android.compose.animation.scene.content.state.TransitionState
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
/**
* Transition to [target] using a canned animation. This function will try to be smart and take over
@@ -50,7 +45,7 @@
return when (transitionState) {
is TransitionState.Idle -> {
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
@@ -80,13 +75,11 @@
} else {
// The transition is in progress: start the canned animation at the same
// progress as it was in.
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
isInitiatedByUserInput,
- initialProgress = progress,
- initialVelocity = transitionState.progressVelocity,
replacedTransition = transitionState,
)
}
@@ -102,13 +95,11 @@
layoutState.finishTransition(transitionState, target)
null
} else {
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
isInitiatedByUserInput,
- initialProgress = progress,
- initialVelocity = transitionState.progressVelocity,
reversed = true,
replacedTransition = transitionState,
)
@@ -140,7 +131,7 @@
animateToScene(layoutState, animateFrom, transitionKey = null)
}
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
@@ -154,103 +145,68 @@
}
}
-private fun CoroutineScope.animate(
+private fun CoroutineScope.animateToScene(
layoutState: MutableSceneTransitionLayoutStateImpl,
targetScene: SceneKey,
transitionKey: TransitionKey?,
isInitiatedByUserInput: Boolean,
replacedTransition: TransitionState.Transition?,
- initialProgress: Float = 0f,
- initialVelocity: Float = 0f,
reversed: Boolean = false,
fromScene: SceneKey = layoutState.transitionState.currentScene,
chain: Boolean = true,
): TransitionState.Transition {
+ val oneOffAnimation = OneOffAnimation()
val targetProgress = if (reversed) 0f else 1f
val transition =
if (reversed) {
- OneOffTransition(
+ OneOffSceneTransition(
key = transitionKey,
fromScene = targetScene,
toScene = fromScene,
currentScene = targetScene,
isInitiatedByUserInput = isInitiatedByUserInput,
- isUserInputOngoing = false,
replacedTransition = replacedTransition,
+ oneOffAnimation = oneOffAnimation,
)
} else {
- OneOffTransition(
+ OneOffSceneTransition(
key = transitionKey,
fromScene = fromScene,
toScene = targetScene,
currentScene = targetScene,
isInitiatedByUserInput = isInitiatedByUserInput,
- isUserInputOngoing = false,
replacedTransition = replacedTransition,
+ oneOffAnimation = oneOffAnimation,
)
}
- // Change the current layout state to start this new transition. This will compute the
- // TransformationSpec associated to this transition, which we need to initialize the Animatable
- // that will actually animate it.
- layoutState.startTransition(transition, chain)
-
- // The transition now contains the transformation spec that we should use to instantiate the
- // Animatable.
- val animationSpec = transition.transformationSpec.progressSpec
- val visibilityThreshold =
- (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
- val animatable =
- Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
- transition.animatable = it
- }
-
- // Animate the progress to its target value.
- // Important: We start atomically to make sure that we start the coroutine even if it is
- // cancelled right after it is launched, so that finishTransition() is correctly called.
- // Otherwise, this transition will never be stopped and we will never settle to Idle.
- transition.job =
- launch(start = CoroutineStart.ATOMIC) {
- try {
- animatable.animateTo(targetProgress, animationSpec, initialVelocity)
- } finally {
- layoutState.finishTransition(transition, targetScene)
- }
- }
+ animateContent(
+ transition = transition,
+ oneOffAnimation = oneOffAnimation,
+ targetProgress = targetProgress,
+ startTransition = { layoutState.startTransition(transition, chain) },
+ finishTransition = { layoutState.finishTransition(transition, targetScene) },
+ )
return transition
}
-private class OneOffTransition(
+private class OneOffSceneTransition(
override val key: TransitionKey?,
fromScene: SceneKey,
toScene: SceneKey,
override val currentScene: SceneKey,
override val isInitiatedByUserInput: Boolean,
- override val isUserInputOngoing: Boolean,
replacedTransition: TransitionState.Transition?,
+ private val oneOffAnimation: OneOffAnimation,
) : TransitionState.Transition(fromScene, toScene, replacedTransition) {
- /**
- * The animatable used to animate this transition.
- *
- * Note: This is lateinit because we need to first create this Transition object so that
- * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to
- * it, which is need to initialize this Animatable.
- */
- lateinit var animatable: Animatable<Float, AnimationVector1D>
-
- /** The job that is animating [animatable]. */
- lateinit var job: Job
-
override val progress: Float
- get() = animatable.value
+ get() = oneOffAnimation.progress
override val progressVelocity: Float
- get() = animatable.velocity
+ get() = oneOffAnimation.progressVelocity
- override fun finish(): Job = job
+ override val isUserInputOngoing: Boolean = false
+
+ override fun finish(): Job = oneOffAnimation.finish()
}
-
-// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
-// and screen density.
-internal const val ProgressVisibilityThreshold = 1e-3f
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 9b1d4ec..752c93e 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 @@
@Mock
private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock
- private AuthDialogPanelInteractionDetector mPanelInteractionDetector;
- @Mock
private UserManager mUserManager;
@Mock
private LockPatternUtils mLockPatternUtils;
@@ -1059,10 +1057,9 @@
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 @@
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 cd9189b..0000000
--- 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 d1f908d..46b370f 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 @@
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 0000000..5a73fe2
--- /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 b4ff565..f1d08c0 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 @@
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 @@
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 @@
)
val expected =
createTileState(
- activationState = QSTileState.ActivationState.INACTIVE,
+ activationState = QSTileState.ActivationState.UNAVAILABLE,
icon = DEFAULT_DRAWABLE,
)
@@ -173,7 +176,7 @@
}
@Test
- fun failedToLoadIconTileIsInactive() =
+ fun failedToLoadIconTileIsUnavailable() =
with(kosmos) {
testScope.runTest {
val actual =
@@ -187,13 +190,32 @@
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 13d6411..1ea8abc 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 @@
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 @@
* 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 ab184ab..f232d52 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.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 @@
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 420b955..51a70bd 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 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 @@
volumePanelGlobalStateRepository.updateVolumePanelState { it.copy(isVisible = true) }
}
+ private val realDumpManager = DumpManager()
private val testableResources = context.orCreateTestableResources
private lateinit var underTest: VolumePanelViewModel
@@ -124,6 +134,60 @@
}
@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 @@
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 ff89ed9..0000000
--- 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/raw/action_key_edu.json b/packages/SystemUI/res/raw/action_key_edu.json
new file mode 100644
index 0000000..014d837
--- /dev/null
+++ b/packages/SystemUI/res/raw/action_key_edu.json
@@ -0,0 +1 @@
+{"v":"5.12.1","fr":60,"ip":0,"op":181,"w":554,"h":564,"nm":"Trackpad-JSON_ActionKey-EDU","ddd":0,"assets":[{"id":"comp_0","nm":"BlankButton","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"actionKey_themed","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0.288,-0.035,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.579,-0.158],[0.605,0],[1.21,1.21],[0,1.684],[-1.184,1.184],[-1.684,0],[-1.21,-1.21],[0,-1.71],[0.184,-0.553],[0.316,-0.474],[0,0],[0,0]],"o":[[-0.474,0.316],[-0.553,0.158],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.184],[0,0.605],[-0.158,0.553],[0,0],[0,0],[0,0]],"v":[[10.241,12.155],[8.663,12.866],[6.926,13.103],[2.585,11.287],[0.809,6.946],[2.585,2.605],[6.926,0.789],[11.307,2.605],[13.122,6.946],[12.846,8.682],[12.136,10.222],[16.911,14.997],[15.017,16.891]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.184],[-1.684,0],[-1.184,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0],[1.21,1.184],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,11.327],[-16.911,6.985],[-15.096,2.605],[-10.754,0.789],[-6.374,2.605],[-4.558,6.985],[-6.374,11.327],[-10.754,13.142]],"c":true}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[-8.268,9.432],[-7.242,6.985],[-8.268,4.499],[-10.754,3.473],[-13.201,4.499],[-14.188,6.985],[-13.201,9.432],[-10.754,10.419]],"c":true}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[9.413,9.432],[10.439,6.985],[9.413,4.499],[6.926,3.473],[4.479,4.499],[3.453,6.985],[4.479,9.432],[6.926,10.419]],"c":true}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0],[1.21,1.21],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,-6.354],[-16.911,-10.695],[-15.096,-15.076],[-10.754,-16.891],[-6.374,-15.076],[-4.558,-10.695],[-6.374,-6.354],[-10.754,-4.539]],"c":true}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0]],"v":[[2.585,-6.354],[0.77,-10.695],[2.585,-15.076],[6.926,-16.891],[11.307,-15.076],[13.122,-10.695],[11.307,-6.354],[6.926,-4.539]],"c":true}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[-8.268,-8.248],[-7.242,-10.695],[-8.268,-13.182],[-10.754,-14.208],[-13.201,-13.182],[-14.188,-10.695],[-13.201,-8.248],[-10.754,-7.222]],"c":true}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[9.413,-8.248],[10.439,-10.695],[9.413,-13.182],[6.926,-14.208],[4.479,-13.182],[3.453,-10.695],[4.479,-8.248],[6.926,-7.222]],"c":true}},"nm":"Path 8","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"icon","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.33],"y":[0.52]},"t":34,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.36],"y":[0]},"t":37,"s":[100]},{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":40,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.33],"y":[0.52]},"t":124,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.36],"y":[0]},"t":127,"s":[100]},{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":130,"s":[100]},{"t":136,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.752941191196,0.423529416323,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.072,"y":0.635},"o":{"x":0.424,"y":0.112},"t":27,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.313,"y":0.131},"t":39,"s":[40,49.21,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":57,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.07,"y":0.63},"o":{"x":0.42,"y":0.11},"t":117,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.313,"y":0.131},"t":129,"s":[40,49.21,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[40,39.79,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"AllApps_Tray_themed","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-8.859,0]},"a":{"a":0,"k":[277,256.562,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-129.938,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[275.625,25.594]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"560x52","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-151.594,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[15.75,1.969]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"handle","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.05,"y":0.7},"t":45,"s":[277,516,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.214,"y":0.214},"t":75,"s":[277,265.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.999,"y":1},"o":{"x":0.3,"y":0},"t":135,"s":[277,265.422,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[277,516,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[316.969,320.906]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":13.78},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"all apps","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"AK_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Scale Down","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[50]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":135,"s":[50]},{"t":144,"s":[100]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.153,0.153,0.153],"y":[0.074,0.074,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.161,0.161,0.161],"y":[0,0,0]},"t":75,"s":[95,95,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":135,"s":[95,95,100]},{"t":165,"s":[100,100,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"shapes":[],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[133,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[229,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[421,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"actionKey_themed","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"matte","td":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"AllApps_Tray_themed","tt":1,"tp":5,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,282,0]},"a":{"a":0,"k":[277,282,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":554,"h":564,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"AK_LofiLauncher","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file
diff --git a/packages/SystemUI/res/raw/action_key_success.json b/packages/SystemUI/res/raw/action_key_success.json
new file mode 100644
index 0000000..cae7344
--- /dev/null
+++ b/packages/SystemUI/res/raw/action_key_success.json
@@ -0,0 +1 @@
+{"v":"5.12.1","fr":60,"ip":0,"op":50,"w":554,"h":564,"nm":"Trackpad-JSON_ActionKey-Success","ddd":0,"assets":[{"id":"comp_0","nm":"TrackpadAK_Success_Checkmark","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Check Rotate","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":2,"s":[-16]},{"t":20,"s":[6]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[95.049,95.049,100]}},"ao":0,"ip":0,"op":228,"st":-72,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Bounce","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":12,"s":[0]},{"t":36,"s":[-6]}]},"p":{"a":0,"k":[81,127,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.263,0.263,0.833],"y":[1.126,1.126,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.958,0.958,0]},"t":1,"s":[80,80,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.45,0.45,0.167],"y":[0.325,0.325,0]},"t":20,"s":[105,105,100]},{"t":36,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":-0.289},"p":{"a":0,"k":[14.364,-33.591,0]},"a":{"a":0,"k":[-0.125,0,0]},"s":{"a":0,"k":[104.744,104.744,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-1.401,-0.007],[-10.033,11.235]],"o":[[5.954,7.288],[1.401,0.007],[0,0]],"v":[[-28.591,4.149],[-10.73,26.013],[31.482,-21.255]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":3,"s":[0]},{"i":{"x":[0.22],"y":[1]},"o":{"x":[0.001],"y":[0.149]},"t":10,"s":[29]},{"t":27,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":11},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":44,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[95,95,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.275,0.275,0.21],"y":[1.102,1.102,1]},"o":{"x":[0.037,0.037,0.05],"y":[0.476,0.476,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.252,0.252,0.47],"y":[0.159,0.159,0]},"t":16,"s":[120,120,100]},{"t":28,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.32,0.32],"y":[0.11,0.11]},"t":16,"s":[148,148]},{"t":28,"s":[136,136]}]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":88},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Checkbox - Widget","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"actionKey_themed-static","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0.288,-0.035,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.579,-0.158],[0.605,0],[1.21,1.21],[0,1.684],[-1.184,1.184],[-1.684,0],[-1.21,-1.21],[0,-1.71],[0.184,-0.553],[0.316,-0.474],[0,0],[0,0]],"o":[[-0.474,0.316],[-0.553,0.158],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.184],[0,0.605],[-0.158,0.553],[0,0],[0,0],[0,0]],"v":[[10.241,12.155],[8.663,12.866],[6.926,13.103],[2.585,11.287],[0.809,6.946],[2.585,2.605],[6.926,0.789],[11.307,2.605],[13.122,6.946],[12.846,8.682],[12.136,10.222],[16.911,14.997],[15.017,16.891]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.184],[-1.684,0],[-1.184,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0],[1.21,1.184],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,11.327],[-16.911,6.985],[-15.096,2.605],[-10.754,0.789],[-6.374,2.605],[-4.558,6.985],[-6.374,11.327],[-10.754,13.142]],"c":true}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[-8.268,9.432],[-7.242,6.985],[-8.268,4.499],[-10.754,3.473],[-13.201,4.499],[-14.188,6.985],[-13.201,9.432],[-10.754,10.419]],"c":true}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[9.413,9.432],[10.439,6.985],[9.413,4.499],[6.926,3.473],[4.479,4.499],[3.453,6.985],[4.479,9.432],[6.926,10.419]],"c":true}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0],[1.21,1.21],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,-6.354],[-16.911,-10.695],[-15.096,-15.076],[-10.754,-16.891],[-6.374,-15.076],[-4.558,-10.695],[-6.374,-6.354],[-10.754,-4.539]],"c":true}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0]],"v":[[2.585,-6.354],[0.77,-10.695],[2.585,-15.076],[6.926,-16.891],[11.307,-15.076],[13.122,-10.695],[11.307,-6.354],[6.926,-4.539]],"c":true}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[-8.268,-8.248],[-7.242,-10.695],[-8.268,-13.182],[-10.754,-14.208],[-13.201,-13.182],[-14.188,-10.695],[-13.201,-8.248],[-10.754,-7.222]],"c":true}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[9.413,-8.248],[10.439,-10.695],[9.413,-13.182],[6.926,-14.208],[4.479,-13.182],[3.453,-10.695],[4.479,-8.248],[6.926,-7.222]],"c":true}},"nm":"Path 8","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"icon","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.752941191196,0.423529416323,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"BlankButton","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"AK_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Scale Down","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":135,"s":[70]},{"t":144,"s":[100]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.153,0.153,0.153],"y":[0.074,0.074,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.161,0.161,0.161],"y":[0,0,0]},"t":75,"s":[95,95,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":135,"s":[95,95,100]},{"t":165,"s":[100,100,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"shapes":[],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"TrackpadAK_Success_Checkmark","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,198.5,0]},"a":{"a":0,"k":[95,95,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":190,"h":190,"ip":6,"op":50,"st":6,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":389,"s":[100]},{"t":392,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098039216,0.101960784314,0.01568627451,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"actionKey_themed-static","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[421,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[229,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[133,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"AK_LofiLauncher","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index a5fd5b9..2ad6b6a 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 12d881b..c0b6acf 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.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.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 @@
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/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index 317201d..f358ba2 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -125,6 +125,7 @@
taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft);
final int diameter = res.getDimensionPixelSize(mButtonDiameterResource);
+ mKeyButtonView.setDiameter(diameter);
mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
taskbarMarginBottom));
}
@@ -195,6 +196,7 @@
public void updateIcon(int lightIconColor, int darkIconColor) {
mAnimatedDrawable = (AnimatedVectorDrawable) mKeyButtonView.getContext()
.getDrawable(mRotationButtonController.getIconResId());
+ mAnimatedDrawable.setBounds(0, 0, mKeyButtonView.getWidth(), mKeyButtonView.getHeight());
mKeyButtonView.setImageDrawable(mAnimatedDrawable);
mKeyButtonView.setColors(lightIconColor, darkIconColor);
}
@@ -248,8 +250,14 @@
updateDimensionResources();
if (mIsShowing) {
+ updateIcon(mRotationButtonController.getLightIconColor(),
+ mRotationButtonController.getDarkIconColor());
final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams();
mWindowManager.updateViewLayout(mKeyButtonContainer, layoutParams);
+ if (mAnimatedDrawable != null) {
+ mAnimatedDrawable.reset();
+ mAnimatedDrawable.start();
+ }
}
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
index 2145166..75412f9 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
@@ -37,6 +37,7 @@
private static final float BACKGROUND_ALPHA = 0.92f;
private KeyButtonRipple mRipple;
+ private int mDiameter;
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final Configuration mLastConfiguration;
@@ -93,10 +94,25 @@
mRipple.setDarkIntensity(darkIntensity);
}
+ /**
+ * Sets the view's diameter.
+ *
+ * @param diameter the diameter value for the view
+ */
+ void setDiameter(int diameter) {
+ mDiameter = diameter;
+ }
+
@Override
public void draw(Canvas canvas) {
int d = Math.min(getWidth(), getHeight());
canvas.drawOval(0, 0, d, d, mOvalBgPaint);
super.draw(canvas);
}
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ setMeasuredDimension(mDiameter, mDiameter);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 9521be1..723587e 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.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.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.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.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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@Override
public void onOrientationChanged() {
- if (!constraintBp()) {
- updatePositionByCapability(true /* invalidate */);
- }
}
@Override
@@ -538,23 +446,6 @@
}
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 @@
};
}
- 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 @@
}
@Override
+ public String getClassNameIfItIsConfirmDeviceCredentialActivity() {
+ return mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity();
+ }
+
+ @Override
public long getRequestId() {
return mConfig.mRequestId;
}
@@ -878,7 +662,7 @@
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 b466f31..037f5b7 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 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 @@
@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 @@
final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
public void onTaskStackChanged() {
- if (!isOwnerInForeground()) {
+ if (isOwnerInBackground()) {
mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground);
}
}
@@ -227,21 +225,20 @@
}
}
- 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 @@
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 @@
});
mWakefulnessLifecycle = wakefulnessLifecycle;
- mPanelInteractionDetector = panelInteractionDetector;
mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null;
@@ -1229,7 +1224,6 @@
operationId,
requestId,
mWakefulnessLifecycle,
- mPanelInteractionDetector,
mUserManager,
mLockPatternUtils,
viewModel);
@@ -1259,9 +1253,9 @@
}
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 @@
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 @@
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 3fd488c..8611916 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 @@
*/
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 04c2351..0000000
--- 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 02eae9ced..0000000
--- 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 5e2b5ff..6da5e42 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 @@
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 348b423..695707d 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 @@
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 b450896..0000000
--- 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 7ccac03..0b474f8 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.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 43ba097..a20a17f 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.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 @@
/** 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 @@
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 @@
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 @@
!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 @@
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 @@
descriptionView,
customizedViewContainer,
),
- viewsToFadeInOnSizeChange =
- listOf(
- logoView,
- logoDescriptionView,
- titleView,
- subtitleView,
- descriptionView,
- customizedViewContainer,
- indicatorMessageView,
- negativeButton,
- cancelButton,
- retryButton,
- confirmationButton,
- credentialFallbackButton,
- ),
- panelViewController = panelViewController,
jankListener = jankListener,
)
}
@@ -275,7 +231,6 @@
if (!showWithoutIcon) {
PromptIconViewBinder.bind(
iconView,
- iconSizeOverride,
viewModel,
)
}
@@ -329,20 +284,6 @@
}
}
- // 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 @@
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 @@
}
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 b9ec2de..85c3ae3 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.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.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 @@
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 @@
}
}
- 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 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()
+ 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()
- )
- }
- }
- }
- }
-
- // 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
- }
- }
- }
-
- 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
+ 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()
)
}
- }
-
- 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)
- )
- }
-
- 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))
- }
-
- 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)
- }
+ PromptPosition.Left -> {
+ outline.setRoundRect(
+ -cornerRadiusPx,
+ 0,
+ view.width,
+ view.height,
+ cornerRadiusPx.toFloat()
+ )
}
- }
-
- 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
- )
- 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()
- )
-
- 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)
- }
- }
-
- currentSize = size
- currentPosition = position
- notifyAccessibilityChanged()
-
- panelView.invalidateOutline()
- view.invalidate()
- view.requestLayout()
+ PromptPosition.Bottom,
+ PromptPosition.Top -> {
+ outline.setRoundRect(
+ 0,
+ 0,
+ view.width,
+ view.height + cornerRadiusPx,
+ cornerRadiusPx.toFloat()
+ )
}
}
}
}
- } 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
+ // ConstraintSets for animating between prompt sizes
+ val mediumConstraintSet = ConstraintSet()
+ mediumConstraintSet.clone(view as ConstraintLayout)
- // 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
+ 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
+ )
+ 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
+ )
}
-
- // prepare for animated size transitions
- for (v in viewsToHideWhenSmall) {
- v.showContentOrHide(forceHide = size.isSmall)
+ 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 (viewModel.hideSensorIcon.first()) {
- iconHolderView.visibility = View.GONE
+ 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 (currentSize == null && size.isSmall) {
- iconHolderView.alpha = 0f
+ 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
+ )
}
- if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
- viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
- }
+ iconHolderView.layoutParams = iconParams
+ }
+ }
+ }
- // propagate size changes to legacy panel controller and animate
- // transitions
- view.doOnLayout {
- val width = view.measuredWidth
- val height = view.measuredHeight
+ 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)
+ }
+ }
- 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,
- )
+ lifecycleScope.launch {
+ viewModel.guidelineBounds.collect { bounds ->
+ val bottomInset =
+ windowManager.maximumWindowMetrics.windowInsets
+ .getInsets(WindowInsets.Type.navigationBars())
+ .bottom
+ mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset)
- 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)
- }
- }
+ 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)
+ )
+ }
+
+ 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))
+ }
+
+ 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)
+ }
+ }
+ }
- currentSize = size
- view.visibility = View.VISIBLE
- viewModel.setIsIconViewLoaded(false)
- notifyAccessibilityChanged()
+ 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
+ )
+ 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()
+ )
+
+ 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)
}
}
+
+ currentSize = size
+ currentPosition = position
+ notifyAccessibilityChanged()
+
+ panelView.invalidateOutline()
+ view.invalidate()
+ view.requestLayout()
}
}
}
@@ -646,17 +453,6 @@
}
}
-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 @@
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 18e2a56..49f4b05 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 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 @@
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 9e4aaaa..eab3b26 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 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 @@
@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 @@
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 31af126..761c3da 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.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 @@
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 @@
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 214420d..25d43d9 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.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 @@
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 @@
/** 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 @@
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 19ea007..c2a4ee3 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.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 @@
->
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 3273111..79f4568 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.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.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 @@
KeyboardShortcutsModule.class,
KeyguardBlueprintModule.class,
KeyguardSectionsModule.class,
+ KeyboardTouchpadTutorialModule.class,
MediaModule.class,
MediaMuteAwaitConnectionCli.StartableModule.class,
MultiUserUtilsModule.class,
@@ -142,7 +144,7 @@
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 b0f2c18..cbea876 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 @@
@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 0000000..8e6cb07
--- /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/inputdevice/oobe/TouchpadTutorialScreensProvider.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt
new file mode 100644
index 0000000..bd3e771
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 androidx.compose.runtime.Composable
+
+interface TouchpadTutorialScreensProvider {
+
+ @Composable fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit)
+
+ @Composable fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt
new file mode 100644
index 0000000..c5b0ca7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.composable
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+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
+
+@Composable
+fun ActionKeyTutorialScreen(
+ onDoneButtonClicked: () -> Unit,
+ onBack: () -> Unit,
+) {
+ BackHandler(onBack = onBack)
+ val screenConfig = buildScreenConfig()
+ var actionState by remember { mutableStateOf(NOT_STARTED) }
+ Box(
+ modifier =
+ Modifier.fillMaxSize().onKeyEvent { keyEvent: KeyEvent ->
+ // temporary before we can access Action/Meta key
+ if (keyEvent.key == Key.AltLeft && keyEvent.type == KeyEventType.KeyUp) {
+ actionState = FINISHED
+ }
+ true
+ }
+ ) {
+ ActionTutorialContent(actionState, onDoneButtonClicked, screenConfig)
+ }
+}
+
+@Composable
+private fun buildScreenConfig() =
+ TutorialScreenConfig(
+ colors = rememberScreenColors(),
+ strings =
+ TutorialScreenConfig.Strings(
+ titleResId = R.string.tutorial_action_key_title,
+ bodyResId = R.string.tutorial_action_key_guidance,
+ titleSuccessResId = R.string.tutorial_action_key_success_title,
+ bodySuccessResId = R.string.tutorial_action_key_success_body
+ ),
+ animations =
+ TutorialScreenConfig.Animations(
+ educationResId = R.raw.action_key_edu,
+ successResId = R.raw.action_key_success
+ )
+ )
+
+@Composable
+private fun rememberScreenColors(): TutorialScreenConfig.Colors {
+ val primaryFixedDim = LocalAndroidColorScheme.current.primaryFixedDim
+ val secondaryFixedDim = LocalAndroidColorScheme.current.secondaryFixedDim
+ val onSecondaryFixed = LocalAndroidColorScheme.current.onSecondaryFixed
+ val onSecondaryFixedVariant = LocalAndroidColorScheme.current.onSecondaryFixedVariant
+ val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer
+ val dynamicProperties =
+ rememberLottieDynamicProperties(
+ rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
+ rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim),
+ rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed),
+ rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
+ )
+ val screenColors =
+ remember(surfaceContainer, dynamicProperties) {
+ TutorialScreenConfig.Colors(
+ background = onSecondaryFixed,
+ successBackground = surfaceContainer,
+ title = primaryFixedDim,
+ animationColors = dynamicProperties,
+ )
+ }
+ return screenColors
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt
new file mode 100644
index 0000000..c50b7dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.composable
+
+import android.graphics.ColorFilter
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import androidx.annotation.RawRes
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.LottieDynamicProperties
+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.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,
+ IN_PROGRESS,
+ FINISHED
+}
+
+@Composable
+fun ActionTutorialContent(
+ actionState: TutorialActionState,
+ onDoneButtonClicked: () -> Unit,
+ config: TutorialScreenConfig
+) {
+ val animatedColor by
+ animateColorAsState(
+ targetValue =
+ if (actionState == FINISHED) config.colors.successBackground
+ else config.colors.background,
+ animationSpec = tween(durationMillis = 150, easing = LinearEasing),
+ label = "backgroundColor"
+ )
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier =
+ Modifier.fillMaxSize()
+ .drawBehind { drawRect(animatedColor) }
+ .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
+ TutorialDescription(
+ titleTextId =
+ if (actionState == FINISHED) config.strings.titleSuccessResId
+ else config.strings.titleResId,
+ titleColor = config.colors.title,
+ bodyTextId =
+ if (actionState == FINISHED) config.strings.bodySuccessResId
+ else config.strings.bodyResId,
+ modifier = Modifier.weight(1f)
+ )
+ Spacer(modifier = Modifier.width(76.dp))
+ TutorialAnimation(
+ actionState,
+ config,
+ modifier = Modifier.weight(1f).padding(top = 8.dp)
+ )
+ }
+ DoneButton(onDoneButtonClicked = onDoneButtonClicked)
+ }
+}
+
+@Composable
+fun TutorialDescription(
+ @StringRes titleTextId: Int,
+ titleColor: Color,
+ @StringRes bodyTextId: Int,
+ modifier: Modifier = Modifier
+) {
+ Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
+ Text(
+ text = stringResource(id = titleTextId),
+ style = MaterialTheme.typography.displayLarge,
+ color = titleColor
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = bodyTextId),
+ style = MaterialTheme.typography.bodyLarge,
+ color = Color.White
+ )
+ }
+}
+
+@Composable
+fun TutorialAnimation(
+ actionState: TutorialActionState,
+ config: TutorialScreenConfig,
+ modifier: Modifier = Modifier
+) {
+ Box(modifier = modifier.fillMaxWidth()) {
+ AnimatedContent(
+ targetState = actionState,
+ transitionSpec = {
+ if (initialState == NOT_STARTED) {
+ val transitionDurationMillis = 150
+ fadeIn(animationSpec = tween(transitionDurationMillis, easing = LinearEasing))
+ .togetherWith(
+ fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis))
+ )
+ // we explicitly don't want size transform because when targetState
+ // animation is loaded for the first time, AnimatedContent thinks target
+ // size is smaller and tries to shrink initial state animation
+ .using(sizeTransform = null)
+ } else {
+ // empty transition works because all remaining transitions are from IN_PROGRESS
+ // state which shares initial animation frame with both FINISHED and NOT_STARTED
+ EnterTransition.None togetherWith ExitTransition.None
+ }
+ }
+ ) { state ->
+ when (state) {
+ NOT_STARTED ->
+ EducationAnimation(
+ config.animations.educationResId,
+ config.colors.animationColors
+ )
+ IN_PROGRESS ->
+ FrozenSuccessAnimation(
+ config.animations.successResId,
+ config.colors.animationColors
+ )
+ FINISHED ->
+ SuccessAnimation(config.animations.successResId, config.colors.animationColors)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FrozenSuccessAnimation(
+ @RawRes successAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+ LottieAnimation(
+ composition = composition,
+ progress = { 0f }, // animation should freeze on 1st frame
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+private fun EducationAnimation(
+ @RawRes educationAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
+ val progress by
+ animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+private fun SuccessAnimation(
+ @RawRes successAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+ val progress by animateLottieCompositionAsState(composition, iterations = 1)
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+fun rememberColorFilterProperty(
+ layerName: String,
+ color: Color
+): LottieDynamicProperty<ColorFilter> {
+ return rememberLottieDynamicProperty(
+ LottieProperty.COLOR_FILTER,
+ value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP),
+ // "**" below means match zero or more layers, so ** layerName ** means find layer with that
+ // name at any depth
+ keyPath = arrayOf("**", layerName, "**")
+ )
+}
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
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialComponents.kt
rename to packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt
index f2276c8..01ad585 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
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt
rename to packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt
index d76ceb9..0406bb9 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 0000000..3e382d6
--- /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 0000000..39b1ec0
--- /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 cfe64e2..9f46846 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 31ff018..b9b3895 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.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 @@
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 05e1044..b3b8f21 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 @@
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 @@
}
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 3f9c98d..17c5977 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.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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
playSounds(false);
}
- setShowingLocked(false);
+ setShowingLocked(false, "onKeyguardExitFinished: " + reason);
mWakeAndUnlocking = false;
mDismissCallbackRegistry.notifyDismissSucceeded();
resetKeyguardDonePendingLocked();
@@ -3343,6 +3356,9 @@
// 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 @@
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 @@
mSurfaceBehindRemoteAnimationRequested = false;
mKeyguardStateController.notifyKeyguardGoingAway(false);
if (mShowing) {
- setShowingLocked(true, true);
+ setShowingLocked(true, true, "hideSurfaceBehindKeyguard");
}
}
@@ -3799,7 +3816,7 @@
// 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 @@
// 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 @@
}
}
- 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 @@
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 de60c11..797a4ec 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 @@
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 9dc77d3..fb97191 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.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 @@
}
}
- 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 a250b22..91a7f7f 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.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 @@
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 @@
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 @@
bind(alternateBouncerView!!, alternateBouncerDependencies.get())
} else {
removeViewFromWindowManager()
- alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
+ alternateBouncerDependencies.get().viewModel.onRemovedFromWindow()
}
}
}
@@ -144,7 +144,7 @@
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 @@
.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 @@
if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) {
return
}
+
optionallyAddUdfpsViews(
view = view,
udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel,
@@ -202,12 +201,13 @@
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 @@
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 df0b3dc..4908dbd 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.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 @@
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 470f17b..7b0b23f 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 @@
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 @@
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 661da6d..c2b5d98 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 @@
}
/**
+ * 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 @@
*
* 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 @@
*
* 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 @@
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 0af5fea..7731481 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 @@
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/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
index 46aa064..2ce7044 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -27,7 +27,6 @@
import android.window.RemoteTransition
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import com.android.systemui.Flags.pssAppSelectorAbruptExitFix
import com.android.systemui.Flags.pssAppSelectorRecentsSplitScreen
import com.android.systemui.display.naturalBounds
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -160,7 +159,7 @@
private fun createAnimation(task: RecentTask, view: View): ActivityOptions =
- if (pssAppSelectorAbruptExitFix() && task.isForegroundTask) {
+ if (task.isForegroundTask) {
// When the selected task is in the foreground, the scale up animation doesn't work.
// We fallback to the default close animation.
ActivityOptions.makeCustomTaskAnimation(
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 9b8dba1..9fb1d46 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 @@
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 875079c..984228d 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 @@
) : 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 @@
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 eec5d3d..204ead3 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 @@
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/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 5b50133..3ec088c 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.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 @@
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 @@
handleKeyguardEnabledness()
notifyKeyguardDismissCallbacks()
refreshLockscreenEnabled()
+ handleHideAlternateBouncerOnTransitionToGone()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -228,13 +231,16 @@
},
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 @@
)
}
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 @@
!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 @@
.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 bccbb11..f6924f2 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.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 @@
scenes: Set<Scene>,
layoutInsetController: LayoutInsetsController,
sceneDataSourceDelegator: SceneDataSourceDelegator,
+ alternateBouncerDependencies: AlternateBouncerDependencies,
) {
this.viewModel = viewModel
setLayoutInsetsController(layoutInsetController)
@@ -49,6 +53,7 @@
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 d31d6f4..73a8e4c 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.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.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 @@
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 @@
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 @@
}
}
+ 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 b54bf6c..46ac54f 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 @@
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 bc23778..21bbaa5 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.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 @@
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 @@
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 fd08e89..a30b877 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 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 @@
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 @@
}
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 2ba79a8..bfb624a 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 @@
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.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.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 @@
// 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 @@
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 db8cd57..c4fbc37 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.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 @@
} 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 f8f9b77..5486abb 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 @@
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 4368239..bd6a1c0 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 @@
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 @@
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 @@
)
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/TouchpadKeyboardTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadKeyboardTutorialModule.kt
deleted file mode 100644
index 8ba8db4..0000000
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadKeyboardTutorialModule.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-
-@Module
-interface TouchpadKeyboardTutorialModule {
-
- @Binds
- @IntoMap
- @ClassKey(TouchpadTutorialActivity::class)
- fun activity(impl: TouchpadTutorialActivity): Activity
-}
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 0000000..238e8a1
--- /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 b6c2ae7..df95232 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 5980e1d..1c8041f 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.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 9ac2cba..57d7c84 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
@@ -16,57 +16,21 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import android.graphics.ColorFilter
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
import androidx.activity.compose.BackHandler
-import androidx.annotation.RawRes
-import androidx.annotation.StringRes
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.snap
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.compose.LottieAnimation
-import com.airbnb.lottie.compose.LottieCompositionSpec
-import com.airbnb.lottie.compose.LottieConstants
-import com.airbnb.lottie.compose.LottieDynamicProperties
-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.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
@@ -81,6 +45,14 @@
): TouchpadGestureMonitor
}
+fun GestureState.toTutorialActionState(): TutorialActionState {
+ return when (this) {
+ NOT_STARTED -> TutorialActionState.NOT_STARTED
+ IN_PROGRESS -> TutorialActionState.IN_PROGRESS
+ FINISHED -> TutorialActionState.FINISHED
+ }
+}
+
@Composable
fun GestureTutorialScreen(
screenConfig: TutorialScreenConfig,
@@ -104,7 +76,11 @@
)
}
TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
- GestureTutorialContent(gestureState, onDoneButtonClicked, screenConfig)
+ ActionTutorialContent(
+ gestureState.toTutorialActionState(),
+ onDoneButtonClicked,
+ screenConfig
+ )
}
}
@@ -135,169 +111,3 @@
content()
}
}
-
-@Composable
-private fun GestureTutorialContent(
- gestureState: GestureState,
- onDoneButtonClicked: () -> Unit,
- config: TutorialScreenConfig
-) {
- val animatedColor by
- animateColorAsState(
- targetValue =
- if (gestureState == FINISHED) config.colors.successBackground
- else config.colors.background,
- animationSpec = tween(durationMillis = 150, easing = LinearEasing),
- label = "backgroundColor"
- )
- Column(
- verticalArrangement = Arrangement.Center,
- modifier =
- Modifier.fillMaxSize()
- .drawBehind { drawRect(animatedColor) }
- .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
- ) {
- Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
- TutorialDescription(
- titleTextId =
- if (gestureState == FINISHED) config.strings.titleSuccessResId
- else config.strings.titleResId,
- titleColor = config.colors.title,
- bodyTextId =
- if (gestureState == FINISHED) config.strings.bodySuccessResId
- else config.strings.bodyResId,
- modifier = Modifier.weight(1f)
- )
- Spacer(modifier = Modifier.width(76.dp))
- TutorialAnimation(
- gestureState,
- config,
- modifier = Modifier.weight(1f).padding(top = 8.dp)
- )
- }
- DoneButton(onDoneButtonClicked = onDoneButtonClicked)
- }
-}
-
-@Composable
-fun TutorialDescription(
- @StringRes titleTextId: Int,
- titleColor: Color,
- @StringRes bodyTextId: Int,
- modifier: Modifier = Modifier
-) {
- Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
- Text(
- text = stringResource(id = titleTextId),
- style = MaterialTheme.typography.displayLarge,
- color = titleColor
- )
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(id = bodyTextId),
- style = MaterialTheme.typography.bodyLarge,
- color = Color.White
- )
- }
-}
-
-@Composable
-fun TutorialAnimation(
- gestureState: GestureState,
- config: TutorialScreenConfig,
- modifier: Modifier = Modifier
-) {
- Box(modifier = modifier.fillMaxWidth()) {
- AnimatedContent(
- targetState = gestureState,
- transitionSpec = {
- if (initialState == NOT_STARTED && targetState == IN_PROGRESS) {
- val transitionDurationMillis = 150
- fadeIn(animationSpec = tween(transitionDurationMillis, easing = LinearEasing))
- .togetherWith(
- fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis))
- )
- // we explicitly don't want size transform because when targetState
- // animation is loaded for the first time, AnimatedContent thinks target
- // size is smaller and tries to shrink initial state animation
- .using(sizeTransform = null)
- } else {
- // empty transition works because all remaining transitions are from IN_PROGRESS
- // state which shares initial animation frame with both FINISHED and NOT_STARTED
- EnterTransition.None togetherWith ExitTransition.None
- }
- }
- ) { gestureState ->
- when (gestureState) {
- NOT_STARTED ->
- EducationAnimation(
- config.animations.educationResId,
- config.colors.animationColors
- )
- IN_PROGRESS ->
- FrozenSuccessAnimation(
- config.animations.successResId,
- config.colors.animationColors
- )
- FINISHED ->
- SuccessAnimation(config.animations.successResId, config.colors.animationColors)
- }
- }
- }
-}
-
-@Composable
-private fun FrozenSuccessAnimation(
- @RawRes successAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
- LottieAnimation(
- composition = composition,
- progress = { 0f }, // animation should freeze on 1st frame
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-private fun EducationAnimation(
- @RawRes educationAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
- val progress by
- animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
- LottieAnimation(
- composition = composition,
- progress = { progress },
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-private fun SuccessAnimation(
- @RawRes successAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
- val progress by animateLottieCompositionAsState(composition, iterations = 1)
- LottieAnimation(
- composition = composition,
- progress = { progress },
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-fun rememberColorFilterProperty(
- layerName: String,
- color: Color
-): LottieDynamicProperty<ColorFilter> {
- return rememberLottieDynamicProperty(
- LottieProperty.COLOR_FILTER,
- value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP),
- // "**" below means match zero or more layers, so ** layerName ** means find layer with that
- // name at any depth
- keyPath = arrayOf("**", layerName, "**")
- )
-}
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 ed3110c..0a6283a 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.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 14355fa..65b452a 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.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 ad8ab30..256c5b5 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,9 +27,11 @@
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
+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
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.ACTION_KEY
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.HOME_GESTURE
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.TUTORIAL_SELECTION
@@ -71,7 +73,7 @@
TutorialSelectionScreen(
onBackTutorialClicked = { vm.goTo(BACK_GESTURE) },
onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) },
- onActionKeyTutorialClicked = {},
+ onActionKeyTutorialClicked = { vm.goTo(ACTION_KEY) },
onDoneButtonClicked = closeTutorial
)
BACK_GESTURE ->
@@ -84,5 +86,10 @@
onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
onBack = { vm.goTo(TUTORIAL_SELECTION) },
)
+ ACTION_KEY -> // TODO(b/358105049) move action key tutorial to OOBE flow
+ ActionKeyTutorialScreen(
+ onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
+ onBack = { vm.goTo(TUTORIAL_SELECTION) },
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
index 11984af..d3aeaa7 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
@@ -55,4 +55,5 @@
TUTORIAL_SELECTION,
BACK_GESTURE,
HOME_GESTURE,
+ ACTION_KEY,
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
index 56b46624..32f2ca6 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 @@
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 9304a46..a426da9 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.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 @@
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 e17274c..ade6c3d 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 @@
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 @@
}
/**
- * 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 @@
}
}
- 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 @@
}
}
}
+
+/**
+ * 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 5d8b6f1..d39daaf 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 @@
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 e46ce26..24fb001 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 @@
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.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 @@
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 5301b00..9de862a 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 @@
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.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 @@
enabledComponents: Collection<VolumePanelComponentKey>,
defaultCriteria: Provider<ComponentAvailabilityCriteria>,
@VolumePanelScope coroutineScope: CoroutineScope,
+ private val logger: VolumePanelLogger,
private val criteriaByKey:
Map<
VolumePanelComponentKey,
@@ -57,12 +63,18 @@
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 cc513b5..276326c 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.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 1c51236..a06d3e3 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 @@
/** 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 5f4dbfb..41c80fa 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 @@
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 f495a02f..2f60c4b 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 @@
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.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 @@
.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 @@
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 @@
}
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 @@
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 @@
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 @@
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 869a82a..d6b159e 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 @@
/** 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 @@
{ "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 dc69cda..f94a6f2 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.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 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 @@
@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 @@
}
@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 @@
}
@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 @@
@Test
fun testShowCredentialUI_withVerticalListContentView() {
- mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP)
mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val container =
initializeFingerprintContainer(
@@ -488,7 +464,6 @@
@Test
fun testShowCredentialUI_withContentViewWithMoreOptionsButton() {
- mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP)
mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val contentView =
PromptContentViewWithMoreOptionsButton.Builder()
@@ -674,7 +649,6 @@
fingerprintProps,
faceProps,
wakefulnessLifecycle,
- panelInteractionDetector,
userManager,
lockPatternUtils,
interactionJankMonitor,
@@ -690,7 +664,6 @@
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 0231486..0000000
--- 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 720f207..dc499cd 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 @@
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 534f25c..6047e7d 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.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 @@
}
@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 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun descriptionOverriddenByContentViewWithMoreOptionsButton() =
runGenericTest(
description = "test description",
@@ -1410,7 +1409,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
} // 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 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_bottom_largeScreen() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -1549,7 +1545,6 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
} // 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 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_right_onlyShortTitle() =
runGenericTest(subtitle = "") {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
@@ -1617,7 +1607,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_left() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
@@ -1634,7 +1623,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_left_onlyShortTitle() =
runGenericTest(subtitle = "") {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
@@ -1649,7 +1637,6 @@
}
@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 3b2cf61..0db7b62 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 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 @@
@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 @@
.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 @@
@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 @@
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 @@
.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 e3a38a8..37f1a3d 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 @@
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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
mViewMediator.onSystemReady();
processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true);
+ mViewMediator.setShowingLocked(true, "");
RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{
mock(RemoteAnimationTarget.class)
@@ -1123,7 +1123,7 @@
@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 @@
@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 @@
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 2af4d87..bfe89de 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.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 @@
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 @@
}
@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 @@
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 @@
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 @@
}
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 @@
}
}
- 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 313292a..d8e96bc 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 @@
@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/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt
index b337ccf..8fbd3c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt
@@ -24,7 +24,6 @@
import android.view.ViewGroup
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX
import com.android.systemui.Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -141,27 +140,13 @@
}
@Test
- fun onRecentAppClicked_fullScreenTaskInForeground_flagOff_usesScaleUpAnimation() {
- mSetFlagsRule.disableFlags(FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX)
-
- controller.onRecentAppClicked(fullScreenTask, taskView)
-
- assertThat(getStartedTaskActivityOptions(fullScreenTask.taskId).animationType)
- .isEqualTo(ActivityOptions.ANIM_SCALE_UP)
- }
-
- @Test
- fun onRecentAppClicked_fullScreenTaskInForeground_flagOn_usesDefaultAnimation() {
- mSetFlagsRule.enableFlags(FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX)
+ fun onRecentAppClicked_fullScreenTaskInForeground_usesDefaultAnimation() {
assertForegroundTaskUsesDefaultCloseAnimation(fullScreenTask)
}
@Test
fun onRecentAppClicked_splitScreenTaskInForeground_flagOn_usesDefaultAnimation() {
- mSetFlagsRule.enableFlags(
- FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX,
- FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN
- )
+ mSetFlagsRule.enableFlags(FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN)
assertForegroundTaskUsesDefaultCloseAnimation(splitScreenTask)
}
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 11b0bdf..7dae5cc 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.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 @@
@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 @@
}
@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 @@
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 @@
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 2919d3f..1e95fc1 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.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 @@
alternateBouncerDependencies = { alternateBouncerDependencies },
windowManager = { windowManager },
layoutInflater = { mockedLayoutInflater },
- dismissCallbackRegistry = dismissCallbackRegistry,
)
}
+@ExperimentalCoroutinesApi
private val Kosmos.alternateBouncerDependencies by
Kosmos.Fixture {
AlternateBouncerDependencies(
@@ -69,6 +67,7 @@
)
}
+@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 bdd4afa..2958315 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 @@
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 0000000..73b1859
--- /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/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 8e76a0b..53b6a2e 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 @@
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 @@
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 2ba1211..0b438d1 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 @@
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 a18f498..3804a9f 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.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 @@
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 34a008f..c4fb9e4 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 @@
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 @@
volumePanelComponentFactory,
configurationController,
broadcastDispatcher,
+ dumpManager,
+ volumePanelLogger,
volumePanelGlobalStateInteractor,
)
}
diff --git a/services/Android.bp b/services/Android.bp
index ded7379..0006455 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -120,6 +120,7 @@
":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 @@
"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 0000000..f8ee823
--- /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 0000000..f30e770
--- /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 2e1416b..d4f729c 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 @@
}
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 @@
|| (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 @@
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 @@
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 @@
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 9d8f337..4a18cb1 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 @@
}
@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 @@
: ApplicationExitInfo.REASON_USER_REQUESTED;
return forceStopPackageLocked(packageName, appId, callerWillRestart, purgeCache, doit,
evenPersistent, uninstalling, packageStateStopped, userId, reasonString, reason);
-
}
@GuardedBy("this")
@@ -4393,6 +4402,16 @@
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 @@
}
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 @@
}
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 @@
}
@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 30efa3e..e57fe13 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 @@
@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 @@
}
}
- 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 @@
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 @@
return;
}
uss.setState(UserState.STATE_SHUTDOWN);
+ mDoNotAbortShutdownUserIds.remove(userId);
}
TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("setUserState-STATE_SHUTDOWN-" + userId + "-[stopUser]");
@@ -1555,7 +1585,8 @@
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 @@
}
}
+ 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 @@
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 @@
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 @@
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 @@
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 @@
}.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 539dbca..2ce4623 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 @@
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 df29ca4..fb8a81b 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 @@
}
}
+ // LINT.IfChange
+
/**
* Checks if a client package is running in the background.
*
@@ -618,4 +620,6 @@
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 a44e553..66e61c0 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 @@
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 268d835..d495ef5 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 @@
for (ZenRule automaticRule : config.automaticRules.values()) {
if (automaticRule.component != null) {
evaluateRule(automaticRule, current, trigger, processSubscriptions, false);
- updateSnoozing(automaticRule);
+ automaticRule.reconsiderConditionOverride();
}
}
@@ -187,13 +187,4 @@
+ 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 8c280ed..db48835 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_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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
return modified;
} else {
if (rule.enabled != azr.isEnabled()) {
- rule.snoozing = false;
+ rule.resetConditionOverride();
}
rule.name = azr.getName();
rule.condition = null;
@@ -1573,18 +1599,16 @@
// 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 @@
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 @@
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 98e3e24..22b4d5d 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 @@
(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 @@
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 c40608d..c95d88e 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 46bd7af..fe0cf59 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 @@
/** 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 8f36118..407f3d9 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 @@
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 f2f5eda..0d6778c 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 @@
@GuardedBy("mLock")
private boolean mVibrateOn;
@GuardedBy("mLock")
- private boolean mKeyboardVibrationOn;
- @GuardedBy("mLock")
private int mRingerMode;
@GuardedBy("mLock")
private boolean mOnWirelessCharger;
@@ -532,14 +529,6 @@
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 @@
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 @@
return "VibrationSettings{"
+ "mVibratorConfig=" + mVibrationConfig
+ ", mVibrateOn=" + mVibrateOn
- + ", mKeyboardVibrationOn=" + mKeyboardVibrationOn
+ ", mVibrateInputDevices=" + mVibrateInputDevices
+ ", mBatterySaverMode=" + mBatterySaverMode
+ ", mRingerMode=" + ringerModeToString(mRingerMode)
@@ -671,7 +660,6 @@
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 @@
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 @@
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 dd66809..8179d6a 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 @@
private int mVibratorSetAmplitudeCount;
private int mVibratorSetExternalControlCount;
private int mVibratorPerformCount;
+ private int mVibratorPerformVendorCount;
private int mVibratorComposeCount;
private int mVibratorComposePwleCount;
@@ -239,6 +240,11 @@
}
}
+ /** 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 @@
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 @@
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 @@
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 d38edfc..4290051 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 @@
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 @@
mTransparentPolicy, mAppCompatOverrides);
mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord,
wmService.mAppCompatConfiguration);
+ mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(mActivityRecord);
}
@NonNull
@@ -113,6 +116,11 @@
}
@NonNull
+ AppCompatLetterboxPolicy getAppCompatLetterboxPolicy() {
+ return mAppCompatLetterboxPolicy;
+ }
+
+ @NonNull
AppCompatFocusOverrides getAppCompatFocusOverrides() {
return mAppCompatOverrides.getAppCompatFocusOverrides();
}
@@ -127,4 +135,9 @@
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 0000000..24ed14c
--- /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 0000000..48a9311
--- /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 80bbee3..2f03105 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 @@
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 @@
mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord,
appCompatConfiguration, optPropBuilder);
mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder);
+ mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord,
+ appCompatConfiguration);
}
@NonNull
@@ -85,4 +89,9 @@
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 0244d27..91205fc 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.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 @@
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 e18ca85..5d8a96c 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 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 @@
}
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 3fc5eaf..252590e 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 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 @@
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 @@
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 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 @@
* 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 @@
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 @@
mSurfaceFrameRelative.height());
t.reparent(mSurface, mParentSurface);
- mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.getAsBoolean();
+ mHasWallpaperBackground = mAppCompatLetterboxOverrides
+ .hasWallpaperBackgroundForLetterbox();
updateAlphaAndBlur(t);
t.show(mSurface);
@@ -460,17 +445,19 @@
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 @@
// 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 4740fc4..0e33734 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 @@
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 @@
@NonNull
private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
@NonNull
- private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy;
+ private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy;
@NonNull
- private final TransparentPolicy mTransparentPolicy;
- @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 @@
// 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 @@
* 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 @@
// 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 @@
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 @@
+ 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 561ff7d..7212d37 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 @@
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 34b9913..2c58c61 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 @@
/**
* @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 58c48ad..c6d0b72 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 @@
}
}
}
- 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 @@
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 9e8811f..09c54cb 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.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.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 @@
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 @@
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 0d14c9f..1b9f8d1 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.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 @@
}
}
+ 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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 dbab54b..a8e350b 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 @@
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 @@
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 @@
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 fad10f7..5518082 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 @@
+ " <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 @@
+ " <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 f6e1162..af7f703 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.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 @@
}
@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 60c4ac7..e70ed5f 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.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 @@
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 @@
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 @@
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 @@
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 @@
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 9af0021..91eb2ed 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 @@
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 @@
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 776a840..212e61e 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_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_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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 72ef888..6f06050 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 @@
USAGE_PHYSICAL_EMULATION,
USAGE_RINGTONE,
USAGE_TOUCH,
+ USAGE_IME_FEEDBACK
};
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -525,7 +526,7 @@
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 @@
@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 @@
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 @@
@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 @@
// 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 @@
// 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/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
index ffaa2d8..400fe8b 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.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 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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 695068a..04997f8 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 @@
// 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 @@
// 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 @@
// 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 aa997ac..3148808 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 @@
.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 2b611b7..18255b8 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 @@
@Test
public void testReorderWithParents() {
/*
- root
+ default TDA
____|______
| |
firstTda secondTda
@@ -1496,10 +1496,12 @@
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 @@
.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 @@
}
@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 39e4f2f..3cc32c6 100644
--- a/telephony/java/android/telephony/PcoData.java
+++ b/telephony/java/android/telephony/PcoData.java
@@ -19,6 +19,8 @@
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 @@
@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 db167c0..127bbff 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -1211,6 +1211,8 @@
.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 c132792..6c3eae1 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -244,6 +244,10 @@
return verbose_;
}
+ void SetVerbose(bool verbose) {
+ verbose_ = verbose;
+ }
+
int GetMinSdkVersion() override {
return min_sdk_;
}
@@ -388,6 +392,8 @@
}
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 0000000..2cebfe9
--- /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 0000000..9bfda45
--- /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 0000000..630bc07
--- /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 0000000..645d500
--- /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 0000000..547d2cb
--- /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();
+ }
+}