Merge "Remove discretization from DiscreteRegistry.dump()" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 7d9e95b..4e34b63 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -172,6 +172,7 @@
// Window
aconfig_declarations {
name: "com.android.window.flags.window-aconfig",
+ exportable: true,
package: "com.android.window.flags",
container: "system",
srcs: ["core/java/android/window/flags/*.aconfig"],
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 2587969..5f32ba0 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -43,29 +43,14 @@
],
out: [
"ravenwood.jar",
-
- // Following files are created just as FYI.
- "hoststubgen_framework-minus-apex_keep_all.txt",
- "hoststubgen_framework-minus-apex_dump.txt",
-
"hoststubgen_framework-minus-apex.log",
- "hoststubgen_framework-minus-apex_stats.csv",
- "hoststubgen_framework-minus-apex_apis.csv",
],
}
framework_minus_apex_cmd = "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
-
"--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
"--out-impl-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
-
"--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
"--policy-override-file $(location :ravenwood-framework-policies) " +
"--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) "
@@ -133,13 +118,26 @@
// Build framework-minus-apex.ravenwood-base without sharding.
// We extract the various dump files from this one, rather than the sharded ones, because
// some dumps use the output from other classes (e.g. base classes) which may not be in the
-// same shard.
+// same shard. Also some of the dump files ("apis") may be slow even when sharded, because
+// the output contains the information from all the input classes, rather than the output classes.
// Not using sharding is fine for this module because it's only used for collecting the
// dump / stats files, which don't have to happen regularly.
java_genrule {
name: "framework-minus-apex.ravenwood-base_all",
defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd,
+ cmd: framework_minus_apex_cmd +
+ "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
+ "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
+
+ "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
+ "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ",
+
+ out: [
+ "hoststubgen_framework-minus-apex_keep_all.txt",
+ "hoststubgen_framework-minus-apex_dump.txt",
+ "hoststubgen_framework-minus-apex_stats.csv",
+ "hoststubgen_framework-minus-apex_apis.csv",
+ ],
}
// Marge all the sharded jars
diff --git a/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java b/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java
index c00c8d5..06cd942 100644
--- a/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java
+++ b/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java
@@ -36,7 +36,7 @@
@RunWith(AndroidJUnit4.class)
public final class SettingsProviderPerfTest {
- private static final String NAMESPACE = "test@namespace";
+ private static final String NAMESPACE = "testing";
private static final String SETTING_NAME1 = "test:setting1";
private static final String SETTING_NAME2 = "test-setting2";
private static final String UNSET_SETTING = "test_unset_setting";
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 18ee6f2..ba66ff7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4441,6 +4441,11 @@
public void run() {
ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
+ synchronized (mLock) {
+ mLastTimeChangeClockTime = mInjector.getCurrentTimeMillis();
+ mLastTimeChangeRealtime = mInjector.getElapsedRealtimeMillis();
+ }
+
while (true) {
int result = mInjector.waitForAlarm();
final long nowRTC = mInjector.getCurrentTimeMillis();
@@ -4464,10 +4469,9 @@
expectedClockTime = lastTimeChangeClockTime
+ (nowELAPSED - mLastTimeChangeRealtime);
}
- if (lastTimeChangeClockTime == 0 || nowRTC < (expectedClockTime - 1000)
+ if (nowRTC < (expectedClockTime - 1000)
|| nowRTC > (expectedClockTime + 1000)) {
- // The change is by at least +/- 1000 ms (or this is the first change),
- // let's do it!
+ // The change is by at least +/- 1000 ms, let's do it!
if (DEBUG_BATCH) {
Slog.v(TAG, "Time changed notification from kernel; rebatching");
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 90de7ab..4fb35c3 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -7312,7 +7312,7 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void finish(int finishTask) {
if (DEBUG_FINISH_ACTIVITY) {
- Log.d("Instrumentation", "finishActivity: finishTask=" + finishTask, new Throwable());
+ Log.d(Instrumentation.TAG, "finishActivity: finishTask=" + finishTask, new Throwable());
}
if (mParent == null) {
int resultCode;
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 74e9583..be70de2 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -16,6 +16,7 @@
package android.app;
+import static android.app.Instrumentation.DEBUG_FINISH_ACTIVITY;
import static android.app.WindowConfiguration.activityTypeToString;
import static android.app.WindowConfiguration.windowingModeToString;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -80,6 +81,7 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
+import android.util.Log;
import android.util.Singleton;
import android.util.Size;
import android.view.WindowInsetsController.Appearance;
@@ -6011,6 +6013,10 @@
* Finishes all activities in this task and removes it from the recent tasks list.
*/
public void finishAndRemoveTask() {
+ if (DEBUG_FINISH_ACTIVITY) {
+ Log.d(Instrumentation.TAG, "AppTask#finishAndRemoveTask: task="
+ + getTaskInfo(), new Throwable());
+ }
try {
mAppTaskImpl.finishAndRemoveTask();
} catch (RemoteException e) {
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/Instrumentation.java b/core/java/android/app/Instrumentation.java
index be27046..45852c7 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -98,7 +98,7 @@
*/
public static final String REPORT_KEY_STREAMRESULT = "stream";
- private static final String TAG = "Instrumentation";
+ static final String TAG = "Instrumentation";
private static final long CONNECT_TIMEOUT_MILLIS = 60_000;
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index fd4d8e9..0cc210b 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -1836,9 +1836,10 @@
// have shared library asset paths appended if there are any.
if (r.getImpl() != null) {
final ResourcesImpl oldImpl = r.getImpl();
+ final AssetManager oldAssets = oldImpl.getAssets();
// ResourcesImpl constructor will help to append shared library asset paths.
- if (oldImpl.getAssets().isUpToDate()) {
- final ResourcesImpl newImpl = new ResourcesImpl(oldImpl.getAssets(),
+ if (oldAssets != AssetManager.getSystem() && oldAssets.isUpToDate()) {
+ final ResourcesImpl newImpl = new ResourcesImpl(oldAssets,
oldImpl.getMetrics(), oldImpl.getConfiguration(),
oldImpl.getDisplayAdjustments());
r.setImpl(newImpl);
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index c789af3..9148e3c 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -37,7 +37,6 @@
}
}
-
flag {
name: "onboarding_bugreport_v2_enabled"
is_exported: true
@@ -403,3 +402,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "dont_read_policy_definition"
+ namespace: "enterprise"
+ description: "Rely on <policy-key-entry> to determine policy definition and ignore <policy-definition-entry>"
+ bug: "335663055"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/app/appfunctions/OWNERS b/core/java/android/app/appfunctions/OWNERS
new file mode 100644
index 0000000..c6827cc
--- /dev/null
+++ b/core/java/android/app/appfunctions/OWNERS
@@ -0,0 +1,6 @@
+avayvod@google.com
+oadesina@google.com
+toki@google.com
+tonymak@google.com
+mingweiliao@google.com
+anothermark@google.com
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index e370e85..273519b 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -151,6 +151,16 @@
}
flag {
+ name: "fix_avatar_cross_user_leak"
+ namespace: "multiuser"
+ description: "Fix cross-user picture uri leak for avatar picker apps."
+ bug: "341688848"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_get_user_property_cache"
namespace: "multiuser"
description: "Cache is not optimised for getUserProperty for values below 0, eg. UserHandler.USER_NULL or UserHandle.USER_ALL"
@@ -278,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"
@@ -386,4 +403,7 @@
description: "Refactorings related to unicorn mode to work on HSUM mode (Read only flag)"
bug: "339201286"
is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 678bd6b..de1cac4 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -415,7 +415,7 @@
@RequiresPermission(TEST_BIOMETRIC)
public BiometricTestSession createTestSession(int sensorId) {
try {
- return new BiometricTestSession(mContext, sensorId,
+ return new BiometricTestSession(mContext, getSensorProperties(), sensorId,
(context, sensorId1, callback) -> mService
.createTestSession(sensorId1, callback, context.getOpPackageName()));
} catch (RemoteException e) {
diff --git a/core/java/android/hardware/biometrics/BiometricTestSession.java b/core/java/android/hardware/biometrics/BiometricTestSession.java
index 027d101..8bd3528 100644
--- a/core/java/android/hardware/biometrics/BiometricTestSession.java
+++ b/core/java/android/hardware/biometrics/BiometricTestSession.java
@@ -27,12 +27,15 @@
import android.util.ArraySet;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Common set of interfaces to test biometric-related APIs, including {@link BiometricPrompt} and
* {@link android.hardware.fingerprint.FingerprintManager}.
+ *
* @hide
*/
@TestApi
@@ -48,21 +51,29 @@
@NonNull ITestSessionCallback callback) throws RemoteException;
}
- private final Context mContext;
private final int mSensorId;
- private final ITestSession mTestSession;
+ private final List<ITestSession> mTestSessionsForAllSensors = new ArrayList<>();
+ private ITestSession mTestSession;
// Keep track of users that were tested, which need to be cleaned up when finishing.
- @NonNull private final ArraySet<Integer> mTestedUsers;
+ @NonNull
+ private final ArraySet<Integer> mTestedUsers;
// Track the users currently cleaning up, and provide a latch that gets notified when all
// users have finished cleaning up. This is an imperfect system, as there can technically be
// multiple cleanups per user. Theoretically we should track the cleanup's BaseClientMonitor's
// unique ID, but it's complicated to plumb it through. This should be fine for now.
- @Nullable private CountDownLatch mCloseLatch;
- @NonNull private final ArraySet<Integer> mUsersCleaningUp;
+ @Nullable
+ private CountDownLatch mCloseLatch;
+ @NonNull
+ private final ArraySet<Integer> mUsersCleaningUp;
- private final ITestSessionCallback mCallback = new ITestSessionCallback.Stub() {
+ private class TestSessionCallbackIml extends ITestSessionCallback.Stub {
+ private final int mSensorId;
+ private TestSessionCallbackIml(int sensorId) {
+ mSensorId = sensorId;
+ }
+
@Override
public void onCleanupStarted(int userId) {
Log.d(getTag(), "onCleanupStarted, sensor: " + mSensorId + ", userId: " + userId);
@@ -76,19 +87,30 @@
mUsersCleaningUp.remove(userId);
if (mUsersCleaningUp.isEmpty() && mCloseLatch != null) {
+ Log.d(getTag(), "counting down");
mCloseLatch.countDown();
}
}
- };
+ }
/**
* @hide
*/
- public BiometricTestSession(@NonNull Context context, int sensorId,
- @NonNull TestSessionProvider testSessionProvider) throws RemoteException {
- mContext = context;
+ public BiometricTestSession(@NonNull Context context, List<SensorProperties> sensors,
+ int sensorId, @NonNull TestSessionProvider testSessionProvider) throws RemoteException {
mSensorId = sensorId;
- mTestSession = testSessionProvider.createTestSession(context, sensorId, mCallback);
+ // When any of the sensors should create the test session, all the other sensors should
+ // set test hal enabled too.
+ for (SensorProperties sensor : sensors) {
+ final int id = sensor.getSensorId();
+ final ITestSession session = testSessionProvider.createTestSession(context, id,
+ new TestSessionCallbackIml(id));
+ mTestSessionsForAllSensors.add(session);
+ if (id == sensorId) {
+ mTestSession = session;
+ }
+ }
+
mTestedUsers = new ArraySet<>();
mUsersCleaningUp = new ArraySet<>();
setTestHalEnabled(true);
@@ -107,8 +129,11 @@
@RequiresPermission(TEST_BIOMETRIC)
private void setTestHalEnabled(boolean enabled) {
try {
- Log.w(getTag(), "setTestHalEnabled, sensor: " + mSensorId + " enabled: " + enabled);
- mTestSession.setTestHalEnabled(enabled);
+ for (ITestSession session : mTestSessionsForAllSensors) {
+ Log.w(getTag(), "setTestHalEnabled, sensor: " + session.getSensorId() + " enabled: "
+ + enabled);
+ session.setTestHalEnabled(enabled);
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -175,10 +200,12 @@
/**
* Simulates an acquired message from the HAL.
*
- * @param userId User that this command applies to.
+ * @param userId User that this command applies to.
* @param acquireInfo See
- * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and
- * {@link FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)}
+ * {@link
+ * BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and
+ * {@link
+ * FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)}
*/
@RequiresPermission(TEST_BIOMETRIC)
public void notifyAcquired(int userId, int acquireInfo) {
@@ -192,10 +219,12 @@
/**
* Simulates an error message from the HAL.
*
- * @param userId User that this command applies to.
+ * @param userId User that this command applies to.
* @param errorCode See
- * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} and
- * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int, CharSequence)}
+ * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int,
+ * CharSequence)} and
+ * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int,
+ * CharSequence)}
*/
@RequiresPermission(TEST_BIOMETRIC)
public void notifyError(int userId, int errorCode) {
@@ -220,8 +249,20 @@
Log.w(getTag(), "Cleanup already in progress for user: " + userId);
}
- mUsersCleaningUp.add(userId);
- mTestSession.cleanupInternalState(userId);
+ for (ITestSession session : mTestSessionsForAllSensors) {
+ mUsersCleaningUp.add(userId);
+ Log.d(getTag(), "cleanupInternalState for sensor: " + session.getSensorId());
+ mCloseLatch = new CountDownLatch(1);
+ session.cleanupInternalState(userId);
+
+ try {
+ Log.d(getTag(), "Awaiting latch...");
+ mCloseLatch.await(3, TimeUnit.SECONDS);
+ Log.d(getTag(), "Finished awaiting");
+ } catch (InterruptedException e) {
+ Log.e(getTag(), "Latch interrupted", e);
+ }
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -234,18 +275,9 @@
// Cleanup can be performed using the test HAL, since it always responds to enumerate with
// zero enrollments.
if (!mTestedUsers.isEmpty()) {
- mCloseLatch = new CountDownLatch(1);
for (int user : mTestedUsers) {
cleanupInternalState(user);
}
-
- try {
- Log.d(getTag(), "Awaiting latch...");
- mCloseLatch.await(3, TimeUnit.SECONDS);
- Log.d(getTag(), "Finished awaiting");
- } catch (InterruptedException e) {
- Log.e(getTag(), "Latch interrupted", e);
- }
}
if (!mUsersCleaningUp.isEmpty()) {
diff --git a/core/java/android/hardware/biometrics/ITestSession.aidl b/core/java/android/hardware/biometrics/ITestSession.aidl
index df9f504..bd99606 100644
--- a/core/java/android/hardware/biometrics/ITestSession.aidl
+++ b/core/java/android/hardware/biometrics/ITestSession.aidl
@@ -59,4 +59,8 @@
// HAL is disabled (e.g. to clean up after a test).
@EnforcePermission("TEST_BIOMETRIC")
void cleanupInternalState(int userId);
+
+ // Get the sensor id of the current test session.
+ @EnforcePermission("TEST_BIOMETRIC")
+ int getSensorId();
}
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index 0c55ed5..9bd4860 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -17,8 +17,6 @@
package android.hardware.camera2.params;
-import static com.android.internal.util.Preconditions.*;
-
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -32,8 +30,6 @@
import android.hardware.camera2.CameraDevice.CameraDeviceSetup;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.impl.CameraMetadataNative;
-import android.hardware.camera2.params.InputConfiguration;
-import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.utils.HashCodeHelpers;
import android.media.ImageReader;
import android.os.Parcel;
@@ -46,6 +42,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -95,8 +92,8 @@
public @interface SessionMode {};
// Camera capture session related parameters.
- private List<OutputConfiguration> mOutputConfigurations;
- private CameraCaptureSession.StateCallback mStateCallback;
+ private final @NonNull List<OutputConfiguration> mOutputConfigurations;
+ private CameraCaptureSession.StateCallback mStateCallback = null;
private int mSessionType;
private Executor mExecutor = null;
private InputConfiguration mInputConfig = null;
@@ -268,7 +265,8 @@
*/
@Override
public int hashCode() {
- return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(), mInputConfig.hashCode(),
+ return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(),
+ Objects.hashCode(mInputConfig),
mSessionType);
}
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 903e916..7f1cac0 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -172,7 +172,7 @@
@RequiresPermission(TEST_BIOMETRIC)
public BiometricTestSession createTestSession(int sensorId) {
try {
- return new BiometricTestSession(mContext, sensorId,
+ return new BiometricTestSession(mContext, getSensorProperties(), sensorId,
(context, sensorId1, callback) -> mService
.createTestSession(sensorId1, callback, context.getOpPackageName()));
} catch (RemoteException e) {
diff --git a/core/java/android/os/ExternalVibrationScale.aidl b/core/java/android/os/ExternalVibrationScale.aidl
index cf6f8ed..644bece 100644
--- a/core/java/android/os/ExternalVibrationScale.aidl
+++ b/core/java/android/os/ExternalVibrationScale.aidl
@@ -33,12 +33,24 @@
SCALE_VERY_HIGH = 2
}
+ // TODO(b/345186129): remove this once we finish migrating to scale factor.
/**
* The scale level that will be applied to external vibrations.
*/
ScaleLevel scaleLevel = ScaleLevel.SCALE_NONE;
/**
+ * The scale factor that will be applied to external vibrations.
+ *
+ * Values in (0,1) will scale down the vibrations, values > 1 will scale up vibrations within
+ * hardware limits. A zero scale factor indicates the external vibration should be muted.
+ *
+ * TODO(b/345186129): update this once we finish migrating, negative should not be expected.
+ * Negative values should be ignored in favour of the legacy ScaleLevel.
+ */
+ float scaleFactor = -1f; // undefined
+
+ /**
* The adaptive haptics scale that will be applied to external vibrations.
*/
float adaptiveHapticsScale = 1f;
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/os/UserManager.java b/core/java/android/os/UserManager.java
index 06c516a..28f2c25 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -4824,6 +4824,7 @@
* <p>Note that this does not alter the user's pre-existing user restrictions.
*
* @param userId the id of the user to become admin
+ * @throws SecurityException if changing ADMIN status of the user is not allowed
* @hide
*/
@RequiresPermission(allOf = {
@@ -4844,6 +4845,7 @@
* <p>Note that this does not alter the user's pre-existing user restrictions.
*
* @param userId the id of the user to revoke admin rights from
+ * @throws SecurityException if changing ADMIN status of the user is not allowed
* @hide
*/
@RequiresPermission(allOf = {
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index f3ef9e1..e68b746 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -663,6 +663,15 @@
* @hide
*/
public static float scale(float intensity, float scaleFactor) {
+ if (Flags.hapticsScaleV2Enabled()) {
+ if (Float.compare(scaleFactor, 1) <= 0 || Float.compare(intensity, 0) == 0) {
+ // Scaling down or scaling zero intensity is straightforward.
+ return scaleFactor * intensity;
+ }
+ // Using S * x / (1 + (S - 1) * x^2) as the scale up function to converge to 1.0.
+ return (scaleFactor * intensity) / (1 + (scaleFactor - 1) * intensity * intensity);
+ }
+
// Applying gamma correction to the scale factor, which is the same as encoding the input
// value, scaling it, then decoding the scaled value.
float scale = MathUtils.pow(scaleFactor, 1f / SCALE_GAMMA);
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index a4164e9..e6e5a27 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -49,8 +49,22 @@
*/
public class VibrationConfig {
+ /**
+ * Hardcoded default scale level gain to be applied between each scale level to define their
+ * scale factor value.
+ *
+ * <p>Default gain defined as 3 dBs.
+ */
+ private static final float DEFAULT_SCALE_LEVEL_GAIN = 1.4f;
+
+ /**
+ * Hardcoded default amplitude to be used when device config is invalid, i.e. not in [1,255].
+ */
+ private static final int DEFAULT_AMPLITUDE = 255;
+
// TODO(b/191150049): move these to vibrator static config file
private final float mHapticChannelMaxVibrationAmplitude;
+ private final int mDefaultVibrationAmplitude;
private final int mRampStepDurationMs;
private final int mRampDownDurationMs;
private final int mRequestVibrationParamsTimeoutMs;
@@ -75,8 +89,10 @@
/** @hide */
public VibrationConfig(@Nullable Resources resources) {
+ mDefaultVibrationAmplitude = resources.getInteger(
+ com.android.internal.R.integer.config_defaultVibrationAmplitude);
mHapticChannelMaxVibrationAmplitude = loadFloat(resources,
- com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude, 0);
+ com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude);
mRampDownDurationMs = loadInteger(resources,
com.android.internal.R.integer.config_vibrationWaveformRampDownDuration, 0);
mRampStepDurationMs = loadInteger(resources,
@@ -87,9 +103,9 @@
com.android.internal.R.array.config_requestVibrationParamsForUsages);
mIgnoreVibrationsOnWirelessCharger = loadBoolean(resources,
- com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false);
+ com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger);
mKeyboardVibrationSettingsSupported = loadBoolean(resources,
- com.android.internal.R.bool.config_keyboardVibrationSettingsSupported, false);
+ com.android.internal.R.bool.config_keyboardVibrationSettingsSupported);
mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources,
com.android.internal.R.integer.config_defaultAlarmVibrationIntensity);
@@ -115,16 +131,16 @@
return value;
}
- private static float loadFloat(@Nullable Resources res, int resId, float defaultValue) {
- return res != null ? res.getFloat(resId) : defaultValue;
+ private static float loadFloat(@Nullable Resources res, int resId) {
+ return res != null ? res.getFloat(resId) : 0f;
}
private static int loadInteger(@Nullable Resources res, int resId, int defaultValue) {
return res != null ? res.getInteger(resId) : defaultValue;
}
- private static boolean loadBoolean(@Nullable Resources res, int resId, boolean defaultValue) {
- return res != null ? res.getBoolean(resId) : defaultValue;
+ private static boolean loadBoolean(@Nullable Resources res, int resId) {
+ return res != null && res.getBoolean(resId);
}
private static int[] loadIntArray(@Nullable Resources res, int resId) {
@@ -145,6 +161,26 @@
}
/**
+ * Return the device default vibration amplitude value to replace the
+ * {@link android.os.VibrationEffect#DEFAULT_AMPLITUDE} constant.
+ */
+ public int getDefaultVibrationAmplitude() {
+ if (mDefaultVibrationAmplitude < 1 || mDefaultVibrationAmplitude > 255) {
+ return DEFAULT_AMPLITUDE;
+ }
+ return mDefaultVibrationAmplitude;
+ }
+
+ /**
+ * Return the device default gain to be applied between scale levels to define the scale factor
+ * for each level.
+ */
+ public float getDefaultVibrationScaleLevelGain() {
+ // TODO(b/356407380): add device config for this
+ return DEFAULT_SCALE_LEVEL_GAIN;
+ }
+
+ /**
* The duration, in milliseconds, that should be applied to the ramp to turn off the vibrator
* when a vibration is cancelled or finished at non-zero amplitude.
*/
@@ -233,6 +269,7 @@
public String toString() {
return "VibrationConfig{"
+ "mIgnoreVibrationsOnWirelessCharger=" + mIgnoreVibrationsOnWirelessCharger
+ + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude
+ ", mHapticChannelMaxVibrationAmplitude=" + mHapticChannelMaxVibrationAmplitude
+ ", mRampStepDurationMs=" + mRampStepDurationMs
+ ", mRampDownDurationMs=" + mRampDownDurationMs
@@ -258,6 +295,7 @@
pw.println("VibrationConfig:");
pw.increaseIndent();
pw.println("ignoreVibrationsOnWirelessCharger = " + mIgnoreVibrationsOnWirelessCharger);
+ pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude);
pw.println("hapticChannelMaxAmplitude = " + mHapticChannelMaxVibrationAmplitude);
pw.println("rampStepDurationMs = " + mRampStepDurationMs);
pw.println("rampDownDurationMs = " + mRampDownDurationMs);
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 1a19bb2..53a1a67d 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -103,3 +103,13 @@
purpose: PURPOSE_FEATURE
}
}
+
+flag {
+ namespace: "haptics"
+ name: "haptics_scale_v2_enabled"
+ description: "Enables new haptics scaling function across all usages"
+ bug: "345186129"
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index a26c6f4..a95ce79 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -104,7 +104,7 @@
public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer,
@Flags int flags) throws IOException {
// Serialize effect first to fail early.
- XmlSerializedVibration<VibrationEffect> serializedVibration =
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration =
toSerializedVibration(effect, flags);
TypedXmlSerializer xmlSerializer = Xml.newFastSerializer();
xmlSerializer.setFeature(XML_FEATURE_INDENT_OUTPUT, (flags & FLAG_PRETTY_PRINT) != 0);
@@ -114,9 +114,9 @@
xmlSerializer.endDocument();
}
- private static XmlSerializedVibration<VibrationEffect> toSerializedVibration(
+ private static XmlSerializedVibration<? extends VibrationEffect> toSerializedVibration(
VibrationEffect effect, @Flags int flags) throws SerializationFailedException {
- XmlSerializedVibration<VibrationEffect> serializedVibration;
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration;
int serializerFlags = 0;
if ((flags & FLAG_ALLOW_HIDDEN_APIS) != 0) {
serializerFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
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/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index a7641c0..9e4b27d 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -3574,7 +3574,7 @@
checkPreconditions(sc);
if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
- "reparent", this, sc,
+ "setColor", this, sc,
"r=" + color[0] + " g=" + color[1] + " b=" + color[2]);
}
nativeSetColor(mNativeObject, sc.mNativeObject, color);
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 4eb7c1c..9aeccf4 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -65,6 +65,14 @@
}
flag {
+ name: "keyguard_going_away_timeout"
+ namespace: "windowing_frontend"
+ description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting"
+ bug: "343598832"
+ is_fixed_read_only: true
+}
+
+flag {
name: "close_to_square_config_includes_status_bar"
namespace: "windowing_frontend"
description: "On close to square display, when necessary, configuration includes status bar"
diff --git a/core/java/com/android/internal/graphics/ColorUtils.java b/core/java/com/android/internal/graphics/ColorUtils.java
index f72a5ca..f210741 100644
--- a/core/java/com/android/internal/graphics/ColorUtils.java
+++ b/core/java/com/android/internal/graphics/ColorUtils.java
@@ -21,7 +21,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.graphics.Color;
-
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import com.android.internal.graphics.cam.Cam;
/**
@@ -29,6 +29,7 @@
*
* A set of color-related utility methods, building upon those available in {@code Color}.
*/
+@RavenwoodKeepWholeClass
public final class ColorUtils {
private static final double XYZ_WHITE_REFERENCE_X = 95.047;
@@ -696,4 +697,4 @@
double calculateContrast(int foreground, int background, int alpha);
}
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/graphics/cam/Cam.java b/core/java/com/android/internal/graphics/cam/Cam.java
index 1df85c3..49fa37b 100644
--- a/core/java/com/android/internal/graphics/cam/Cam.java
+++ b/core/java/com/android/internal/graphics/cam/Cam.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import com.android.internal.graphics.ColorUtils;
@@ -25,6 +26,7 @@
* A color appearance model, based on CAM16, extended to use L* as the lightness dimension, and
* coupled to a gamut mapping algorithm. Creates a color system, enables a digital design system.
*/
+@RavenwoodKeepWholeClass
public class Cam {
// The maximum difference between the requested L* and the L* returned.
private static final float DL_MAX = 0.2f;
diff --git a/core/java/com/android/internal/graphics/cam/CamUtils.java b/core/java/com/android/internal/graphics/cam/CamUtils.java
index f541729..76fabc6 100644
--- a/core/java/com/android/internal/graphics/cam/CamUtils.java
+++ b/core/java/com/android/internal/graphics/cam/CamUtils.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.graphics.Color;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import com.android.internal.graphics.ColorUtils;
@@ -45,6 +46,7 @@
* consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of
* Color Psychology, 2015
*/
+@RavenwoodKeepWholeClass
public final class CamUtils {
private CamUtils() {
}
diff --git a/core/java/com/android/internal/graphics/cam/Frame.java b/core/java/com/android/internal/graphics/cam/Frame.java
index 0ac7cbc..c419fab 100644
--- a/core/java/com/android/internal/graphics/cam/Frame.java
+++ b/core/java/com/android/internal/graphics/cam/Frame.java
@@ -17,6 +17,7 @@
package com.android.internal.graphics.cam;
import android.annotation.NonNull;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import android.util.MathUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +34,7 @@
* number of calculations during the color => CAM conversion process that depend only on the viewing
* conditions. Caching those calculations in a Frame instance saves a significant amount of time.
*/
+@RavenwoodKeepWholeClass
public final class Frame {
// Standard viewing conditions assumed in RGB specification - Stokes, Anderson, Chandrasekar,
// Motta - A Standard Default Color Space for the Internet: sRGB, 1996.
diff --git a/core/java/com/android/internal/graphics/cam/HctSolver.java b/core/java/com/android/internal/graphics/cam/HctSolver.java
index d7a8691..6e558e7 100644
--- a/core/java/com/android/internal/graphics/cam/HctSolver.java
+++ b/core/java/com/android/internal/graphics/cam/HctSolver.java
@@ -16,6 +16,8 @@
package com.android.internal.graphics.cam;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+
/**
* An efficient algorithm for determining the closest sRGB color to a set of HCT coordinates,
* based on geometrical insights for finding intersections in linear RGB, CAM16, and L*a*b*.
@@ -24,6 +26,7 @@
* Copied from //java/com/google/ux/material/libmonet/hct on May 22 2022.
* ColorUtils/MathUtils functions that were required were added to CamUtils.
*/
+@RavenwoodKeepWholeClass
public class HctSolver {
private HctSolver() {}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index fbec1f1..e0c90d8 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -332,6 +332,8 @@
}
private void onTracingFlush() {
+ Log.d(LOG_TAG, "Executing onTracingFlush");
+
final ExecutorService loggingService;
try {
mBackgroundServiceLock.lock();
@@ -352,15 +354,19 @@
Log.e(LOG_TAG, "Failed to wait for tracing to finish", e);
}
- dumpTransitionTraceConfig();
+ dumpViewerConfig();
+
+ Log.d(LOG_TAG, "Finished onTracingFlush");
}
- private void dumpTransitionTraceConfig() {
+ private void dumpViewerConfig() {
if (mViewerConfigInputStreamProvider == null) {
// No viewer config available
return;
}
+ Log.d(LOG_TAG, "Dumping viewer config to trace");
+
ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
if (pis == null) {
@@ -390,6 +396,8 @@
Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump on tracing end", e);
}
});
+
+ Log.d(LOG_TAG, "Dumped viewer config to trace");
}
private static void writeViewerConfigGroup(
@@ -770,6 +778,8 @@
private synchronized void onTracingInstanceStart(
int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+ Log.d(LOG_TAG, "Executing onTracingInstanceStart");
+
final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) {
mDefaultLogLevelCounts[i]++;
@@ -800,10 +810,13 @@
mCacheUpdater.run();
this.mTracingInstances.incrementAndGet();
+
+ Log.d(LOG_TAG, "Finished onTracingInstanceStart");
}
private synchronized void onTracingInstanceStop(
int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+ Log.d(LOG_TAG, "Executing onTracingInstanceStop");
this.mTracingInstances.decrementAndGet();
final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
@@ -835,6 +848,7 @@
}
mCacheUpdater.run();
+ Log.d(LOG_TAG, "Finished onTracingInstanceStop");
}
private static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
index 15ecedd..cd7dcfd 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
@@ -29,7 +29,7 @@
import android.util.IntArray;
import android.util.LongArray;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java
similarity index 86%
rename from core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java
rename to core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java
index 23df304..6c562c9 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java
@@ -29,24 +29,24 @@
import java.util.Arrays;
/**
- * Serialized representation of a {@link VibrationEffect}.
+ * Serialized representation of a {@link VibrationEffect.Composed}.
*
* <p>The vibration is represented by a list of serialized segments that can be added to a
* {@link VibrationEffect.Composition} during the {@link #deserialize()} procedure.
*
* @hide
*/
-final class SerializedVibrationEffect implements XmlSerializedVibration<VibrationEffect> {
+final class SerializedComposedEffect implements XmlSerializedVibration<VibrationEffect.Composed> {
@NonNull
private final SerializedSegment[] mSegments;
- SerializedVibrationEffect(@NonNull SerializedSegment segment) {
+ SerializedComposedEffect(@NonNull SerializedSegment segment) {
requireNonNull(segment);
mSegments = new SerializedSegment[]{ segment };
}
- SerializedVibrationEffect(@NonNull SerializedSegment[] segments) {
+ SerializedComposedEffect(@NonNull SerializedSegment[] segments) {
requireNonNull(segments);
checkArgument(segments.length > 0, "Unsupported empty vibration");
mSegments = segments;
@@ -54,12 +54,12 @@
@NonNull
@Override
- public VibrationEffect deserialize() {
+ public VibrationEffect.Composed deserialize() {
VibrationEffect.Composition composition = VibrationEffect.startComposition();
for (SerializedSegment segment : mSegments) {
segment.deserializeIntoComposition(composition);
}
- return composition.compose();
+ return (VibrationEffect.Composed) composition.compose();
}
@Override
@@ -79,7 +79,7 @@
@Override
public String toString() {
- return "SerializedVibrationEffect{"
+ return "SerializedComposedEffect{"
+ "segments=" + Arrays.toString(mSegments)
+ '}';
}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java
index db5c7ff..862f7cb 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java
@@ -27,7 +27,7 @@
import android.os.VibrationEffect;
import android.os.vibrator.PrimitiveSegment;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PrimitiveEffectName;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java
index 8924311..a6f48a4 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java
@@ -25,7 +25,7 @@
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java
new file mode 100644
index 0000000..aa1b0a23
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java
@@ -0,0 +1,127 @@
+/*
+ * 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.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.os.PersistableBundle;
+import android.os.VibrationEffect;
+import android.text.TextUtils;
+import android.util.Base64;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Serialized representation of a {@link VibrationEffect.VendorEffect}.
+ *
+ * <p>The vibration is represented by an opaque {@link PersistableBundle} that can be used by
+ * {@link VibrationEffect#createVendorEffect(PersistableBundle)} during the {@link #deserialize()}
+ * procedure.
+ *
+ * @hide
+ */
+final class SerializedVendorEffect implements XmlSerializedVibration<VibrationEffect.VendorEffect> {
+
+ @NonNull
+ private final PersistableBundle mVendorData;
+
+ SerializedVendorEffect(@NonNull PersistableBundle vendorData) {
+ requireNonNull(vendorData);
+ mVendorData = vendorData;
+ }
+
+ @SuppressLint("MissingPermission")
+ @NonNull
+ @Override
+ public VibrationEffect.VendorEffect deserialize() {
+ return (VibrationEffect.VendorEffect) VibrationEffect.createVendorEffect(mVendorData);
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer)
+ throws IOException {
+ serializer.startTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VIBRATION_EFFECT);
+ writeContent(serializer);
+ serializer.endTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VIBRATION_EFFECT);
+ }
+
+ @Override
+ public void writeContent(@NonNull TypedXmlSerializer serializer) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ mVendorData.writeToStream(outputStream);
+
+ serializer.startTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VENDOR_EFFECT);
+ serializer.text(Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP));
+ serializer.endTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VENDOR_EFFECT);
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedVendorEffect{"
+ + "vendorData=" + mVendorData
+ + '}';
+ }
+
+ /** Parser implementation for {@link SerializedVendorEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedVendorEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_VENDOR_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+
+ PersistableBundle vendorData;
+ XmlReader.readNextText(parser, TAG_VENDOR_EFFECT);
+
+ try {
+ String text = parser.getText().trim();
+ XmlValidator.checkParserCondition(!text.isEmpty(),
+ "Expected tag %s to have base64 representation of vendor data, got empty",
+ TAG_VENDOR_EFFECT);
+
+ vendorData = PersistableBundle.readFromStream(
+ new ByteArrayInputStream(Base64.decode(text, Base64.DEFAULT)));
+ XmlValidator.checkParserCondition(!vendorData.isEmpty(),
+ "Expected tag %s to have non-empty vendor data, got empty bundle",
+ TAG_VENDOR_EFFECT);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new XmlParserException(
+ TextUtils.formatSimple(
+ "Expected base64 representation of vendor data in tag %s, got %s",
+ TAG_VENDOR_EFFECT, parser.getText()),
+ e);
+ } catch (IOException e) {
+ throw new XmlParserException("Error reading vendor data from decoded bytes", e);
+ }
+
+ // Consume tag
+ XmlReader.readEndTag(parser);
+
+ return new SerializedVendorEffect(vendorData);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index 2b8b61d..a9fbcaf 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -18,13 +18,15 @@
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
import android.annotation.NonNull;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.modules.utils.TypedXmlPullParser;
import java.io.IOException;
@@ -80,6 +82,16 @@
* }
* </pre>
*
+ * * Vendor vibration effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <vendor-effect>base64-representation-of-persistable-bundle</vendor-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
* @hide
*/
public class VibrationEffectXmlParser {
@@ -87,11 +99,9 @@
/**
* Parses the current XML tag with all nested tags into a single {@link XmlSerializedVibration}
* wrapping a {@link VibrationEffect}.
- *
- * @see XmlParser#parseTag(TypedXmlPullParser)
*/
@NonNull
- public static XmlSerializedVibration<VibrationEffect> parseTag(
+ public static XmlSerializedVibration<? extends VibrationEffect> parseTag(
@NonNull TypedXmlPullParser parser, @XmlConstants.Flags int flags)
throws XmlParserException, IOException {
XmlValidator.checkStartTag(parser, TAG_VIBRATION_EFFECT);
@@ -107,8 +117,9 @@
* <p>This can be reused for reading a vibration from an XML root tag or from within a combined
* vibration, but it should always be called from places that validates the top level tag.
*/
- static SerializedVibrationEffect parseVibrationContent(TypedXmlPullParser parser,
- @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ private static XmlSerializedVibration<? extends VibrationEffect> parseVibrationContent(
+ TypedXmlPullParser parser, @XmlConstants.Flags int flags)
+ throws XmlParserException, IOException {
String vibrationTagName = parser.getName();
int vibrationTagDepth = parser.getDepth();
@@ -116,11 +127,16 @@
XmlReader.readNextTagWithin(parser, vibrationTagDepth),
"Unsupported empty vibration tag");
- SerializedVibrationEffect serializedVibration;
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration;
switch (parser.getName()) {
+ case TAG_VENDOR_EFFECT:
+ if (Flags.vendorVibrationEffects()) {
+ serializedVibration = SerializedVendorEffect.Parser.parseNext(parser, flags);
+ break;
+ } // else fall through
case TAG_PREDEFINED_EFFECT:
- serializedVibration = new SerializedVibrationEffect(
+ serializedVibration = new SerializedComposedEffect(
SerializedPredefinedEffect.Parser.parseNext(parser, flags));
break;
case TAG_PRIMITIVE_EFFECT:
@@ -128,11 +144,11 @@
do { // First primitive tag already open
primitives.add(SerializedCompositionPrimitive.Parser.parseNext(parser));
} while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
- serializedVibration = new SerializedVibrationEffect(
+ serializedVibration = new SerializedComposedEffect(
primitives.toArray(new SerializedSegment[primitives.size()]));
break;
case TAG_WAVEFORM_EFFECT:
- serializedVibration = new SerializedVibrationEffect(
+ serializedVibration = new SerializedComposedEffect(
SerializedAmplitudeStepWaveform.Parser.parseNext(parser));
break;
default:
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
index f561c14..d74a23d 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
@@ -17,13 +17,15 @@
package com.android.internal.vibrator.persistence;
import android.annotation.NonNull;
+import android.os.PersistableBundle;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName;
import com.android.internal.vibrator.persistence.XmlConstants.PrimitiveEffectName;
@@ -41,6 +43,7 @@
* <li>{@link VibrationEffect#createWaveform(long[], int[], int)}
* <li>A composition created exclusively via
* {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
+ * <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
* </ul>
*
* @hide
@@ -49,13 +52,16 @@
/**
* Creates a serialized representation of the input {@code vibration}.
- *
- * @see XmlSerializer#serialize
*/
@NonNull
- public static XmlSerializedVibration<VibrationEffect> serialize(
+ public static XmlSerializedVibration<? extends VibrationEffect> serialize(
@NonNull VibrationEffect vibration, @XmlConstants.Flags int flags)
throws XmlSerializerException {
+ if (Flags.vendorVibrationEffects()
+ && (vibration instanceof VibrationEffect.VendorEffect vendorEffect)) {
+ return serializeVendorEffect(vendorEffect);
+ }
+
XmlValidator.checkSerializerCondition(vibration instanceof VibrationEffect.Composed,
"Unsupported VibrationEffect type %s", vibration);
@@ -73,7 +79,7 @@
return serializeWaveformEffect(composed);
}
- private static SerializedVibrationEffect serializePredefinedEffect(
+ private static SerializedComposedEffect serializePredefinedEffect(
VibrationEffect.Composed effect, @XmlConstants.Flags int flags)
throws XmlSerializerException {
List<VibrationEffectSegment> segments = effect.getSegments();
@@ -81,10 +87,15 @@
"Unsupported repeating predefined effect %s", effect);
XmlValidator.checkSerializerCondition(segments.size() == 1,
"Unsupported multiple segments in predefined effect %s", effect);
- return new SerializedVibrationEffect(serializePrebakedSegment(segments.get(0), flags));
+ return new SerializedComposedEffect(serializePrebakedSegment(segments.get(0), flags));
}
- private static SerializedVibrationEffect serializePrimitiveEffect(
+ private static SerializedVendorEffect serializeVendorEffect(
+ VibrationEffect.VendorEffect effect) {
+ return new SerializedVendorEffect(effect.getVendorData());
+ }
+
+ private static SerializedComposedEffect serializePrimitiveEffect(
VibrationEffect.Composed effect) throws XmlSerializerException {
List<VibrationEffectSegment> segments = effect.getSegments();
XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
@@ -95,10 +106,10 @@
primitives[i] = serializePrimitiveSegment(segments.get(i));
}
- return new SerializedVibrationEffect(primitives);
+ return new SerializedComposedEffect(primitives);
}
- private static SerializedVibrationEffect serializeWaveformEffect(
+ private static SerializedComposedEffect serializeWaveformEffect(
VibrationEffect.Composed effect) throws XmlSerializerException {
SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder =
new SerializedAmplitudeStepWaveform.Builder();
@@ -120,7 +131,7 @@
segment.getDuration(), toAmplitudeInt(segment.getAmplitude()));
}
- return new SerializedVibrationEffect(serializedWaveformBuilder.build());
+ return new SerializedComposedEffect(serializedWaveformBuilder.build());
}
private static SerializedPredefinedEffect serializePrebakedSegment(
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index 8b92153..2a55d99 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -40,6 +40,7 @@
public static final String TAG_PREDEFINED_EFFECT = "predefined-effect";
public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect";
+ public static final String TAG_VENDOR_EFFECT = "vendor-effect";
public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
public static final String TAG_REPEATING = "repeating";
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlParser.java b/core/java/com/android/internal/vibrator/persistence/XmlParser.java
deleted file mode 100644
index 6712f1c..0000000
--- a/core/java/com/android/internal/vibrator/persistence/XmlParser.java
+++ /dev/null
@@ -1,51 +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.internal.vibrator.persistence;
-
-import android.annotation.NonNull;
-
-import com.android.modules.utils.TypedXmlPullParser;
-
-import java.io.IOException;
-
-/**
- * Parse XML tags into valid {@link XmlSerializedVibration} instances.
- *
- * @param <T> The vibration type that will be parsed.
- * @see XmlSerializedVibration
- * @hide
- */
-@FunctionalInterface
-public interface XmlParser<T> {
-
- /**
- * Parses the current XML tag with all nested tags into a single {@link XmlSerializedVibration}.
- *
- * <p>This method will consume nested XML tags until it finds the
- * {@link TypedXmlPullParser#END_TAG} for the current tag.
- *
- * <p>The vibration reconstructed by the returned {@link XmlSerializedVibration#deserialize()}
- * is guaranteed to be valid. This method will throw an exception otherwise.
- *
- * @param pullParser The {@link TypedXmlPullParser} with the input XML.
- * @return The parsed vibration wrapped in a {@link XmlSerializedVibration} representation.
- * @throws IOException On any I/O error while reading the input XML
- * @throws XmlParserException If the XML content does not represent a valid vibration.
- */
- XmlSerializedVibration<T> parseTag(@NonNull TypedXmlPullParser pullParser)
- throws XmlParserException, IOException;
-}
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlParserException.java b/core/java/com/android/internal/vibrator/persistence/XmlParserException.java
index 7507864..e2b30e7 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlParserException.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlParserException.java
@@ -23,7 +23,6 @@
/**
* Represents an error while parsing a vibration XML input.
*
- * @see XmlParser
* @hide
*/
public final class XmlParserException extends Exception {
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
index a5ace84..0ac6fef 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
@@ -130,6 +130,25 @@
}
/**
+ * Read the next element, ignoring comments and ignorable whitespace, and returns only if it's a
+ * {@link XmlPullParser#TEXT}. Any other tag will fail this check.
+ *
+ * <p>The parser will be pointing to the first next element after skipping comments,
+ * instructions and ignorable whitespace.
+ */
+ public static void readNextText(TypedXmlPullParser parser, String tagName)
+ throws XmlParserException, IOException {
+ try {
+ int type = parser.next(); // skips comments, instruction tokens and ignorable whitespace
+ XmlValidator.checkParserCondition(type == XmlPullParser.TEXT,
+ "Unexpected event %s of type %d, expected text event inside tag %s",
+ parser.getName(), type, tagName);
+ } catch (XmlPullParserException e) {
+ throw XmlParserException.createFromPullParserException("text event", e);
+ }
+ }
+
+ /**
* Check parser has a {@link XmlPullParser#END_TAG} as the next tag, with no nested tags.
*
* <p>The parser will be pointing to the end tag after this method.
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java
index 3233fa2..c20b7d2 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java
@@ -26,8 +26,7 @@
* Serialized representation of a generic vibration.
*
* <p>This can be used to represent a {@link android.os.CombinedVibration} or a
- * {@link android.os.VibrationEffect}. Instances can be created from vibration objects via
- * {@link XmlSerializer}, or from XML content via {@link XmlParser}.
+ * {@link android.os.VibrationEffect}.
*
* <p>The separation of serialization and writing procedures enables configurable rules to define
* which vibrations can be successfully serialized before any data is written to the output stream.
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java
deleted file mode 100644
index 102e6c1..0000000
--- a/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java
+++ /dev/null
@@ -1,40 +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.internal.vibrator.persistence;
-
-import android.annotation.NonNull;
-
-/**
- * Creates a {@link XmlSerializedVibration} instance representing a vibration.
- *
- * @param <T> The vibration type that will be serialized.
- * @see XmlSerializedVibration
- * @hide
- */
-@FunctionalInterface
-public interface XmlSerializer<T> {
-
- /**
- * Creates a serialized representation of the input {@code vibration}.
- *
- * @param vibration The vibration to be serialized
- * @return The serialized representation of the input vibration
- * @throws XmlSerializerException If the input vibration cannot be serialized
- */
- @NonNull
- XmlSerializedVibration<T> serialize(@NonNull T vibration) throws XmlSerializerException;
-}
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java
index c57ff5d..2e7ad09 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java
@@ -19,7 +19,6 @@
/**
* Represents an error while serializing a vibration input.
*
- * @see XmlSerializer
* @hide
*/
public final class XmlSerializerException extends Exception {
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java
index 84d4f3f..1b5a356 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java
@@ -18,7 +18,7 @@
import static java.util.Objects.requireNonNull;
-import android.annotation.NonNull;
+import android.os.VibrationEffect;
import android.text.TextUtils;
import com.android.internal.util.ArrayUtils;
@@ -82,11 +82,11 @@
* Check given {@link XmlSerializedVibration} represents the expected {@code vibration} object
* when it's deserialized.
*/
- @NonNull
- public static <T> void checkSerializedVibration(
- XmlSerializedVibration<T> serializedVibration, T expectedVibration)
+ public static void checkSerializedVibration(
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration,
+ VibrationEffect expectedVibration)
throws XmlSerializerException {
- T deserializedVibration = requireNonNull(serializedVibration.deserialize());
+ VibrationEffect deserializedVibration = requireNonNull(serializedVibration.deserialize());
checkSerializerCondition(Objects.equals(expectedVibration, deserializedVibration),
"Unexpected serialized vibration %s: found deserialization %s, expected %s",
serializedVibration, deserializedVibration, expectedVibration);
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/core/res/res/layout/list_menu_item_icon.xml b/core/res/res/layout/list_menu_item_icon.xml
index a30be6a..5854e81 100644
--- a/core/res/res/layout/list_menu_item_icon.xml
+++ b/core/res/res/layout/list_menu_item_icon.xml
@@ -18,6 +18,8 @@
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:maxWidth="@dimen/list_menu_item_icon_max_width"
+ android:adjustViewBounds="true"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dip"
android:layout_marginEnd="-8dip"
diff --git a/core/res/res/layout/time_picker_text_input_material.xml b/core/res/res/layout/time_picker_text_input_material.xml
index 4988842..86070b1 100644
--- a/core/res/res/layout/time_picker_text_input_material.xml
+++ b/core/res/res/layout/time_picker_text_input_material.xml
@@ -34,19 +34,29 @@
android:layoutDirection="ltr">
<EditText
android:id="@+id/input_hour"
- android:layout_width="50dp"
+ android:layout_width="50sp"
android:layout_height="wrap_content"
+ android:layout_alignEnd="@id/hour_label_holder"
android:inputType="number"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField"
android:imeOptions="actionNext"/>
- <TextView
- android:id="@+id/label_hour"
+ <!-- Ensure the label_hour takes up at least 50sp of space -->
+ <FrameLayout
+ android:id="@+id/hour_label_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@id/input_hour"
- android:layout_alignStart="@id/input_hour"
- android:labelFor="@+id/input_hour"
- android:text="@string/time_picker_hour_label"/>
+ android:layout_below="@id/input_hour">
+ <TextView
+ android:id="@+id/label_hour"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:labelFor="@+id/input_hour"
+ android:text="@string/time_picker_hour_label"/>
+ <Space
+ android:layout_width="50sp"
+ android:layout_height="0dp"/>
+ </FrameLayout>
<TextView
android:id="@+id/input_separator"
@@ -58,21 +68,30 @@
<EditText
android:id="@+id/input_minute"
- android:layout_width="50dp"
+ android:layout_width="50sp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/input_hour"
android:layout_toEndOf="@id/input_separator"
android:inputType="number"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" />
- <TextView
- android:id="@+id/label_minute"
+ <!-- Ensure the label_minute takes up at least 50sp of space -->
+ <FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/input_minute"
android:layout_alignStart="@id/input_minute"
- android:labelFor="@+id/input_minute"
- android:text="@string/time_picker_minute_label"/>
-
+ >
+ <TextView
+ android:id="@+id/label_minute"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:labelFor="@+id/input_minute"
+ android:text="@string/time_picker_minute_label"/>
+ <Space
+ android:layout_width="50sp"
+ android:layout_height="0dp"/>
+ </FrameLayout>
<TextView
android:visibility="invisible"
android:id="@+id/label_error"
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 77b5587..f397ef2 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -1065,4 +1065,7 @@
<!-- The non-linear progress interval when the screen is wider than the
navigation_edge_action_progress_threshold. -->
<item name="back_progress_non_linear_factor" format="float" type="dimen">0.2</item>
+
+ <!-- The maximum width for a context menu icon -->
+ <dimen name="list_menu_item_icon_max_width">24dp</dimen>
</resources>
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 5793bbe..2bbaf9c 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -249,6 +249,7 @@
],
srcs: [
"src/android/app/ActivityManagerTest.java",
+ "src/android/colormodel/CamTest.java",
"src/android/content/ContextTest.java",
"src/android/content/pm/PackageManagerTest.java",
"src/android/content/pm/UserInfoTest.java",
diff --git a/core/tests/coretests/src/android/colormodel/CamTest.java b/core/tests/coretests/src/android/colormodel/CamTest.java
index 05fc0e0..cf398db 100644
--- a/core/tests/coretests/src/android/colormodel/CamTest.java
+++ b/core/tests/coretests/src/android/colormodel/CamTest.java
@@ -18,9 +18,12 @@
import static org.junit.Assert.assertEquals;
+import android.platform.test.ravenwood.RavenwoodRule;
+
import androidx.test.filters.LargeTest;
import org.junit.Assert;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -35,6 +38,9 @@
static final int GREEN = 0xff00ff00;
static final int BLUE = 0xff0000ff;
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
@Test
public void camFromIntToInt() {
Cam cam = Cam.fromInt(RED);
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp
index dd86094..efb8437 100644
--- a/core/tests/resourceflaggingtests/Android.bp
+++ b/core/tests/resourceflaggingtests/Android.bp
@@ -22,54 +22,6 @@
default_team: "trendy_team_android_resources",
}
-genrule {
- name: "resource-flagging-test-app-resources-compile",
- tools: ["aapt2"],
- srcs: [
- "flagged_resources_res/values/bools.xml",
- ],
- out: ["values_bools.arsc.flat"],
- cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
- "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
-}
-
-genrule {
- name: "resource-flagging-test-app-resources-compile2",
- tools: ["aapt2"],
- srcs: [
- "flagged_resources_res/values/bools2.xml",
- ],
- out: ["values_bools2.arsc.flat"],
- cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
- "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
-}
-
-genrule {
- name: "resource-flagging-test-app-apk",
- tools: ["aapt2"],
- // The first input file in the list must be the manifest
- srcs: [
- "TestAppAndroidManifest.xml",
- ":resource-flagging-test-app-resources-compile",
- ":resource-flagging-test-app-resources-compile2",
- ],
- out: ["resapp.apk"],
- cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
-}
-
-java_genrule {
- name: "resource-flagging-apk-as-resource",
- srcs: [
- ":resource-flagging-test-app-apk",
- ],
- out: ["apks_as_resources.res.zip"],
- tools: ["soong_zip"],
-
- cmd: "mkdir -p $(genDir)/res/raw && " +
- "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " +
- "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
-}
-
android_test {
name: "ResourceFlaggingTests",
srcs: [
@@ -82,6 +34,6 @@
"testng",
"compatibility-device-util-axt",
],
- resource_zips: [":resource-flagging-apk-as-resource"],
+ resource_zips: [":resource-flagging-test-app-apk-as-resource"],
test_suites: ["device-tests"],
}
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index ad8542e..c1e3578 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -69,11 +69,23 @@
}
private boolean getBoolean(String name) {
- int resId = mResources.getIdentifier(name, "bool", "com.android.intenal.flaggedresources");
+ int resId = mResources.getIdentifier(
+ name,
+ "bool",
+ "com.android.intenal.flaggedresources");
assertThat(resId).isNotEqualTo(0);
return mResources.getBoolean(resId);
}
+ private String getString(String name) {
+ int resId = mResources.getIdentifier(
+ name,
+ "string",
+ "com.android.intenal.flaggedresources");
+ assertThat(resId).isNotEqualTo(0);
+ return mResources.getString(resId);
+ }
+
private String extractApkAndGetPath(int id) throws Exception {
final Resources resources = mContext.getResources();
try (InputStream is = resources.openRawResource(id)) {
diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
index e9a08ae..97f1d5e 100644
--- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
@@ -27,7 +27,11 @@
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -36,6 +40,9 @@
public class PrimitiveSegmentTest {
private static final float TOLERANCE = 1e-2f;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testCreation() {
PrimitiveSegment primitive = new PrimitiveSegment(
@@ -87,7 +94,8 @@
}
@Test
- public void testScale_fullPrimitiveScaleValue() {
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_fullPrimitiveScaleValue() {
PrimitiveSegment initial = new PrimitiveSegment(
VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0);
@@ -102,7 +110,24 @@
}
@Test
- public void testScale_halfPrimitiveScaleValue() {
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_fullPrimitiveScaleValue() {
+ PrimitiveSegment initial = new PrimitiveSegment(
+ VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0);
+
+ assertEquals(1f, initial.scale(1).getScale(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.5f).getScale(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(1f, initial.scale(1.5f).getScale(), TOLERANCE);
+ assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getScale(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.8f, initial.scale(0.8f).getScale(), TOLERANCE);
+ assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getScale(), TOLERANCE);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_halfPrimitiveScaleValue() {
PrimitiveSegment initial = new PrimitiveSegment(
VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 0);
@@ -117,6 +142,22 @@
}
@Test
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_halfPrimitiveScaleValue() {
+ PrimitiveSegment initial = new PrimitiveSegment(
+ VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 0);
+
+ assertEquals(0.5f, initial.scale(1).getScale(), TOLERANCE);
+ assertEquals(0.25f, initial.scale(0.5f).getScale(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.66f, initial.scale(1.5f).getScale(), TOLERANCE);
+ assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getScale(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.4f, initial.scale(0.8f).getScale(), TOLERANCE);
+ assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getScale(), TOLERANCE);
+ }
+
+ @Test
public void testScale_zeroPrimitiveScaleValue() {
PrimitiveSegment initial = new PrimitiveSegment(
VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 0);
diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
index 01013ab..bea8293 100644
--- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
@@ -29,7 +29,11 @@
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -38,6 +42,9 @@
public class RampSegmentTest {
private static final float TOLERANCE = 1e-2f;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testCreation() {
RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0,
@@ -97,14 +104,18 @@
}
@Test
- public void testScale() {
- RampSegment initial = new RampSegment(0, 1, 0, 0, 0);
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_halfAndFullAmplitudes() {
+ RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0);
- assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE);
assertEquals(0.34f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE);
@@ -117,17 +128,38 @@
}
@Test
- public void testScale_halfPrimitiveScaleValue() {
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_halfAndFullAmplitudes() {
RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0);
assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE);
- assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.25f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.66f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
- assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
- assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.4f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
+
+ assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(1f, initial.scale(1.5f).getEndAmplitude(), TOLERANCE);
+ assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getEndAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
- assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
- assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.81f, initial.scale(0.8f).getEndAmplitude(), TOLERANCE);
+ assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getEndAmplitude(), TOLERANCE);
+ }
+
+ @Test
+ public void testScale_zeroAmplitude() {
+ RampSegment initial = new RampSegment(0, 1, 0, 0, 0);
+
+ assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
}
@Test
diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
index 40776ab..411074a 100644
--- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
@@ -27,7 +27,11 @@
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -35,6 +39,10 @@
@RunWith(JUnit4.class)
public class StepSegmentTest {
private static final float TOLERANCE = 1e-2f;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testCreation() {
StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequencyHz= */ 1f,
@@ -93,7 +101,8 @@
}
@Test
- public void testScale_fullAmplitude() {
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_fullAmplitude() {
StepSegment initial = new StepSegment(1f, 0, 0);
assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE);
@@ -107,7 +116,23 @@
}
@Test
- public void testScale_halfAmplitude() {
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_fullAmplitude() {
+ StepSegment initial = new StepSegment(1f, 0, 0);
+
+ assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(1f, initial.scale(1.5f).getAmplitude(), TOLERANCE);
+ assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getAmplitude(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.8f, initial.scale(0.8f).getAmplitude(), TOLERANCE);
+ assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getAmplitude(), TOLERANCE);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_halfAmplitude() {
StepSegment initial = new StepSegment(0.5f, 0, 0);
assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE);
@@ -121,6 +146,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_halfAmplitude() {
+ StepSegment initial = new StepSegment(0.5f, 0, 0);
+
+ assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE);
+ assertEquals(0.25f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.66f, initial.scale(1.5f).getAmplitude(), TOLERANCE);
+ assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getAmplitude(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.4f, initial.scale(0.8f).getAmplitude(), TOLERANCE);
+ assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getAmplitude(), TOLERANCE);
+ }
+
+ @Test
public void testScale_zeroAmplitude() {
StepSegment initial = new StepSegment(0, 0, 0);
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index bf9a820..1cc38de 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -24,22 +24,32 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
+import android.os.PersistableBundle;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Xml;
import com.android.modules.utils.TypedXmlPullParser;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.xmlpull.v1.XmlPullParser;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -53,6 +63,9 @@
@RunWith(JUnit4.class)
public class VibrationEffectXmlSerializationTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void isSupportedMimeType_onlySupportsVibrationXmlMimeType() {
// Single MIME type supported
@@ -422,6 +435,97 @@
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testVendorEffect_featureFlagEnabled_allSucceed() throws Exception {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("id", 1);
+ vendorData.putDouble("scale", 0.5);
+ vendorData.putBoolean("loop", false);
+ vendorData.putLongArray("amplitudes", new long[] { 0, 255, 128 });
+ vendorData.putString("label", "vibration");
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ vendorData.writeToStream(outputStream);
+ String vendorDataStr = Base64.getEncoder().encodeToString(outputStream.toByteArray());
+
+ VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData);
+ String xml = "<vibration-effect><vendor-effect> " // test trailing whitespace is ignored
+ + vendorDataStr
+ + " \n </vendor-effect></vibration-effect>";
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, vendorDataStr);
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, vendorDataStr);
+ assertHiddenApisRoundTrip(effect);
+
+ // Check PersistableBundle from round-trip
+ PersistableBundle parsedVendorData =
+ ((VibrationEffect.VendorEffect) parseVibrationEffect(serialize(effect),
+ /* flags= */ 0)).getVendorData();
+ assertThat(parsedVendorData.size()).isEqualTo(vendorData.size());
+ assertThat(parsedVendorData.getInt("id")).isEqualTo(1);
+ assertThat(parsedVendorData.getDouble("scale")).isEqualTo(0.5);
+ assertThat(parsedVendorData.getBoolean("loop")).isFalse();
+ assertArrayEquals(parsedVendorData.getLongArray("amplitudes"), new long[] { 0, 255, 128 });
+ assertThat(parsedVendorData.getString("label")).isEqualTo("vibration");
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testInvalidVendorEffect_featureFlagEnabled_allFail() throws IOException {
+ String emptyTag = "<vibration-effect><vendor-effect/></vibration-effect>";
+ assertPublicApisParserFails(emptyTag);
+ assertHiddenApisParserFails(emptyTag);
+
+ String emptyStringTag =
+ "<vibration-effect><vendor-effect> \n </vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(emptyStringTag);
+ assertHiddenApisParserFails(emptyStringTag);
+
+ String invalidString =
+ "<vibration-effect><vendor-effect>invalid</vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(invalidString);
+ assertHiddenApisParserFails(invalidString);
+
+ String validBase64String =
+ "<vibration-effect><vendor-effect>c29tZXNh</vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(validBase64String);
+ assertHiddenApisParserFails(validBase64String);
+
+ PersistableBundle emptyData = new PersistableBundle();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ emptyData.writeToStream(outputStream);
+ String emptyBundleString = "<vibration-effect><vendor-effect>"
+ + Base64.getEncoder().encodeToString(outputStream.toByteArray())
+ + "</vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(emptyBundleString);
+ assertHiddenApisParserFails(emptyBundleString);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testVendorEffect_featureFlagDisabled_allFail() throws Exception {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("id", 1);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ vendorData.writeToStream(outputStream);
+ String vendorDataStr = Base64.getEncoder().encodeToString(outputStream.toByteArray());
+ String xml = "<vibration-effect><vendor-effect>"
+ + vendorDataStr
+ + "</vendor-effect></vibration-effect>";
+ VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData);
+
+ assertPublicApisParserFails(xml);
+ assertPublicApisSerializerFails(vendorEffect);
+
+ assertHiddenApisParserFails(xml);
+ assertHiddenApisSerializerFails(vendorEffect);
+ }
+
private void assertPublicApisParserFails(String xml) {
assertThrows("Expected parseVibrationEffect to fail for " + xml,
VibrationXmlParser.ParseFailedException.class,
@@ -493,6 +597,12 @@
() -> serialize(effect));
}
+ private void assertHiddenApisSerializerFails(VibrationEffect effect) {
+ assertThrows("Expected serialization to fail for " + effect,
+ VibrationXmlSerializer.SerializationFailedException.class,
+ () -> serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS));
+ }
+
private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
String... expectedSegments) throws Exception {
assertSerializationContainsSegments(serialize(effect), expectedSegments);
diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt
index f0e13c4..280b405 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -41,9 +41,11 @@
ctor public VibrationEffect();
method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
+ method public byte[] getVendorEffect_optional();
method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional();
method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
+ method public void setVendorEffect_optional(byte[]);
method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect);
}
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index fcd250b..21a6fac 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -46,6 +46,9 @@
<!-- Predefined vibration effect -->
<xs:element name="predefined-effect" type="PredefinedEffect"/>
+ <!-- Vendor vibration effect -->
+ <xs:element name="vendor-effect" type="VendorEffect"/>
+
<!-- Primitive composition effect -->
<xs:sequence>
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
@@ -136,6 +139,10 @@
</xs:restriction>
</xs:simpleType>
+ <xs:simpleType name="VendorEffect">
+ <xs:restriction base="xs:base64Binary"/>
+ </xs:simpleType>
+
<xs:complexType name="PrimitiveEffect">
<xs:attribute name="name" type="PrimitiveEffectName" use="required"/>
<xs:attribute name="scale" type="PrimitiveScale"/>
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index b9de691..d35d777 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -44,6 +44,9 @@
<!-- Predefined vibration effect -->
<xs:element name="predefined-effect" type="PredefinedEffect"/>
+ <!-- Vendor vibration effect -->
+ <xs:element name="vendor-effect" type="VendorEffect"/>
+
<!-- Primitive composition effect -->
<xs:sequence>
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
@@ -113,6 +116,10 @@
</xs:restriction>
</xs:simpleType>
+ <xs:simpleType name="VendorEffect">
+ <xs:restriction base="xs:base64Binary"/>
+ </xs:simpleType>
+
<xs:complexType name="PrimitiveEffect">
<xs:attribute name="name" type="PrimitiveEffectName" use="required"/>
<xs:attribute name="scale" type="PrimitiveScale"/>
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/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 0a8166f..df5f1e4 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -139,6 +139,10 @@
<string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string>
<!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]-->
<string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
+ <!-- Click action label for bubbles to expand menu. [CHAR LIMIT=30]-->
+ <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>
<!-- 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/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/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 24c568c..b7834db 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
@@ -32,6 +32,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import com.android.wm.shell.R;
@@ -188,12 +189,30 @@
// 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)));
+ }
+ });
}
mMenuViewController = new BubbleBarMenuViewController(mContext, this);
mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
@Override
public void onMenuVisibilityChanged(boolean visible) {
setObscured(visible);
+ if (visible) {
+ mHandleView.setFocusable(false);
+ mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ } else {
+ mHandleView.setFocusable(true);
+ mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
index 8389c81..0300869 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
@@ -23,7 +23,9 @@
import android.graphics.drawable.Icon;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -71,6 +73,16 @@
mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title);
mBubbleDismissIconView = findViewById(R.id.bubble_bar_manage_menu_dismiss_icon);
updateThemeColors();
+
+ mBubbleSectionView.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_collapse_menu)));
+ }
+ });
}
private void updateThemeColors() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS
new file mode 100644
index 0000000..1875675
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS
@@ -0,0 +1,4 @@
+# WM shell sub-module compat ui owners
+mariiasand@google.com
+gracielawputri@google.com
+mcarli@google.com
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/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/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 723a531..428cc91 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -693,16 +693,6 @@
return;
}
- if (mSplitScreenOptional.isPresent()) {
- // If pip activity will reparent to origin task case and if the origin task still
- // under split root, apply exit split transaction to make it expand to fullscreen.
- SplitScreenController split = mSplitScreenOptional.get();
- if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) {
- split.prepareExitSplitScreen(wct, split.getStageOfTask(
- mTaskInfo.lastParentTaskIdBeforePip),
- SplitScreenController.EXIT_REASON_APP_FINISHED);
- }
- }
mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds);
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 7774384..dc21f82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -31,6 +31,8 @@
import android.os.Bundle;
import android.view.InsetsState;
import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import androidx.annotation.Nullable;
@@ -40,6 +42,7 @@
import com.android.internal.util.Preconditions;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
@@ -71,7 +74,8 @@
*/
public class PipController implements ConfigurationChangeListener,
PipTransitionState.PipTransitionStateChangedListener,
- DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> {
+ DisplayController.OnDisplaysChangedListener,
+ DisplayChangeController.OnDisplayChangingListener, RemoteCallable<PipController> {
private static final String TAG = PipController.class.getSimpleName();
private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds";
private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay";
@@ -197,11 +201,12 @@
mPipDisplayLayoutState.setDisplayLayout(layout);
mDisplayController.addDisplayWindowListener(this);
+ mDisplayController.addDisplayChangingController(this);
mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
new DisplayInsetsController.OnInsetsChangedListener() {
@Override
public void insetsChanged(InsetsState insetsState) {
- onDisplayChanged(mDisplayController
+ setDisplayLayout(mDisplayController
.getDisplayLayout(mPipDisplayLayoutState.getDisplayId()));
}
});
@@ -264,11 +269,12 @@
@Override
public void onThemeChanged() {
- onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()));
+ setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay()));
}
//
- // DisplayController.OnDisplaysChangedListener implementations
+ // DisplayController.OnDisplaysChangedListener and
+ // DisplayChangeController.OnDisplayChangingListener implementations
//
@Override
@@ -276,7 +282,7 @@
if (displayId != mPipDisplayLayoutState.getDisplayId()) {
return;
}
- onDisplayChanged(mDisplayController.getDisplayLayout(displayId));
+ setDisplayLayout(mDisplayController.getDisplayLayout(displayId));
}
@Override
@@ -284,10 +290,35 @@
if (displayId != mPipDisplayLayoutState.getDisplayId()) {
return;
}
- onDisplayChanged(mDisplayController.getDisplayLayout(displayId));
+ setDisplayLayout(mDisplayController.getDisplayLayout(displayId));
}
- private void onDisplayChanged(DisplayLayout layout) {
+ /**
+ * A callback for any observed transition that contains a display change in its
+ * {@link android.window.TransitionRequestInfo} with a non-zero rotation delta.
+ */
+ @Override
+ public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t) {
+ if (!mPipTransitionState.isInPip()) {
+ return;
+ }
+
+ // Calculate the snap fraction pre-rotation.
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds());
+
+ // Update the caches to reflect the new display layout and movement bounds.
+ mPipDisplayLayoutState.rotateTo(toRotation);
+ mPipTouchHandler.updateMovementBounds();
+
+ // The policy is to keep PiP width, height and snap fraction invariant.
+ Rect toBounds = mPipBoundsState.getBounds();
+ mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction);
+ mPipBoundsState.setBounds(toBounds);
+ t.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
+ }
+
+ private void setDisplayLayout(DisplayLayout layout) {
mPipDisplayLayoutState.setDisplayLayout(layout);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
index d7c225b..d75fa00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -1081,7 +1081,7 @@
* Updates the current movement bounds based on whether the menu is currently visible and
* resized.
*/
- private void updateMovementBounds() {
+ void updateMovementBounds() {
Rect insetBounds = new Rect();
mPipBoundsAlgorithm.getInsetBounds(insetBounds);
mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 48d17ec6..c11a112 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -439,9 +439,9 @@
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setResizeTransition: hasPendingResize=%b",
mPendingResize != null);
if (mPendingResize != null) {
+ mPendingResize.cancel(null);
mainDecor.cancelRunningAnimations();
sideDecor.cancelRunningAnimations();
- mPendingResize.cancel(null);
mAnimations.clear();
onFinish(null /* wct */);
}
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/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 6d68797..fa905e2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -57,6 +57,7 @@
import android.view.View
import android.view.WindowInsets.Type.statusBars
import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
@@ -356,6 +357,36 @@
}
@Test
+ fun testCloseButtonInFreeform() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+ val windowDecor = setUpMockDecorationForTask(task)
+
+ onTaskOpening(task)
+ val onClickListenerCaptor = argumentCaptor<View.OnClickListener>()
+ verify(windowDecor).setCaptionListeners(
+ onClickListenerCaptor.capture(), any(), any(), any())
+
+ val onClickListener = onClickListenerCaptor.firstValue
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.close_window)
+
+ val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java)
+ desktopModeWindowDecorViewModel
+ .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter)
+
+ onClickListener.onClick(view)
+
+ val transactionCaptor = argumentCaptor<WindowContainerTransaction>()
+ verify(freeformTaskTransitionStarter).startRemoveTransition(transactionCaptor.capture())
+ val wct = transactionCaptor.firstValue
+
+ assertEquals(1, wct.getHierarchyOps().size)
+ assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK,
+ wct.getHierarchyOps().get(0).getType())
+ assertEquals(task.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() {
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
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/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index d184f64..1217b47 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -42,6 +42,9 @@
constexpr bool initialize_gl_always() {
return false;
}
+constexpr bool resample_gainmap_regions() {
+ return false;
+}
} // namespace hwui_flags
#endif
@@ -100,6 +103,7 @@
bool Properties::clipSurfaceViews = false;
bool Properties::hdr10bitPlus = false;
+bool Properties::resampleGainmapRegions = false;
int Properties::timeoutMultiplier = 1;
@@ -175,6 +179,8 @@
clipSurfaceViews =
base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews());
hdr10bitPlus = hwui_flags::hdr_10bit_plus();
+ resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions",
+ hwui_flags::resample_gainmap_regions());
timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index e264642..73e80ce 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -342,6 +342,7 @@
static bool clipSurfaceViews;
static bool hdr10bitPlus;
+ static bool resampleGainmapRegions;
static int timeoutMultiplier;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index cd3ae53..13c0b00 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -97,3 +97,13 @@
description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL."
bug: "335172671"
}
+
+flag {
+ name: "resample_gainmap_regions"
+ namespace: "core_graphics"
+ description: "Resample gainmaps when decoding regions, to improve visual quality"
+ bug: "352847821"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index ea5c144..6a65b82 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -87,8 +87,17 @@
requireUnpremul, prefColorSpace);
}
- bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, int outWidth, int outHeight,
- const SkIRect& desiredSubset, int sampleSize, bool requireUnpremul) {
+ // Decodes the gainmap region. If decoding succeeded, returns true and
+ // populate outGainmap with the decoded gainmap. Otherwise, returns false.
+ //
+ // Note that the desiredSubset is the logical region within the source
+ // gainmap that we want to decode. This is used for scaling into the final
+ // bitmap, since we do not want to include portions of the gainmap outside
+ // of this region. desiredSubset is also _not_ guaranteed to be
+ // pixel-aligned, so it's not possible to simply resize the resulting
+ // bitmap to accomplish this.
+ bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, SkISize bitmapDimensions,
+ const SkRect& desiredSubset, int sampleSize, bool requireUnpremul) {
SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType);
sk_sp<SkColorSpace> decodeColorSpace =
mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr);
@@ -107,13 +116,30 @@
// allocation type. RecyclingClippingPixelAllocator will populate this with the
// actual alpha type in either allocPixelRef() or copyIfNecessary()
sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make(
- outWidth, outHeight, decodeColorType, kPremul_SkAlphaType, decodeColorSpace));
+ bitmapDimensions, decodeColorType, kPremul_SkAlphaType, decodeColorSpace));
if (!nativeBitmap) {
ALOGE("OOM allocating Bitmap for Gainmap");
return false;
}
- RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false);
- if (!mGainmapBRD->decodeRegion(&bm, &allocator, desiredSubset, sampleSize, decodeColorType,
+
+ // Round out the subset so that we decode a slightly larger region, in
+ // case the subset has fractional components.
+ SkIRect roundedSubset = desiredSubset.roundOut();
+
+ // Map the desired subset to the space of the decoded gainmap. The
+ // subset is repositioned relative to the resulting bitmap, and then
+ // scaled to respect the sampleSize.
+ // This assumes that the subset will not be modified by the decoder, which is true
+ // for existing gainmap formats.
+ SkRect logicalSubset = desiredSubset.makeOffset(-std::floorf(desiredSubset.left()),
+ -std::floorf(desiredSubset.top()));
+ logicalSubset.fLeft /= sampleSize;
+ logicalSubset.fTop /= sampleSize;
+ logicalSubset.fRight /= sampleSize;
+ logicalSubset.fBottom /= sampleSize;
+
+ RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false, logicalSubset);
+ if (!mGainmapBRD->decodeRegion(&bm, &allocator, roundedSubset, sampleSize, decodeColorType,
requireUnpremul, decodeColorSpace)) {
ALOGE("Error decoding Gainmap region");
return false;
@@ -130,16 +156,31 @@
return true;
}
- SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion, int* inOutWidth,
- int* inOutHeight) {
+ struct Projection {
+ SkRect srcRect;
+ SkISize destSize;
+ };
+ Projection calculateGainmapRegion(const SkIRect& mainImageRegion, SkISize dimensions) {
const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width();
const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height();
- *inOutWidth *= scaleX;
- *inOutHeight *= scaleY;
- // TODO: Account for rounding error?
- return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
- mainImageRegion.right() * scaleX,
- mainImageRegion.bottom() * scaleY);
+
+ if (uirenderer::Properties::resampleGainmapRegions) {
+ const auto srcRect = SkRect::MakeLTRB(
+ mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
+ mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY);
+ // Request a slightly larger destination size so that the gainmap
+ // subset we want fits entirely in this size.
+ const auto destSize = SkISize::Make(std::ceil(dimensions.width() * scaleX),
+ std::ceil(dimensions.height() * scaleY));
+ return Projection{.srcRect = srcRect, .destSize = destSize};
+ } else {
+ const auto srcRect = SkRect::Make(SkIRect::MakeLTRB(
+ mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
+ mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY));
+ const auto destSize =
+ SkISize::Make(dimensions.width() * scaleX, dimensions.height() * scaleY);
+ return Projection{.srcRect = srcRect, .destSize = destSize};
+ }
}
bool hasGainmap() { return mGainmapBRD != nullptr; }
@@ -327,16 +368,16 @@
sp<uirenderer::Gainmap> gainmap;
bool hasGainmap = brd->hasGainmap();
if (hasGainmap) {
- int gainmapWidth = bitmap.width();
- int gainmapHeight = bitmap.height();
+ SkISize gainmapDims = SkISize::Make(bitmap.width(), bitmap.height());
if (javaBitmap) {
// If we are recycling we must match the inBitmap's relative dimensions
- gainmapWidth = recycledBitmap->width();
- gainmapHeight = recycledBitmap->height();
+ gainmapDims.fWidth = recycledBitmap->width();
+ gainmapDims.fHeight = recycledBitmap->height();
}
- SkIRect gainmapSubset = brd->calculateGainmapRegion(subset, &gainmapWidth, &gainmapHeight);
- if (!brd->decodeGainmapRegion(&gainmap, gainmapWidth, gainmapHeight, gainmapSubset,
- sampleSize, requireUnpremul)) {
+ BitmapRegionDecoderWrapper::Projection gainmapProjection =
+ brd->calculateGainmapRegion(subset, gainmapDims);
+ if (!brd->decodeGainmapRegion(&gainmap, gainmapProjection.destSize,
+ gainmapProjection.srcRect, sampleSize, requireUnpremul)) {
// If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap
hasGainmap = false;
}
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index a88139d..258bf91 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -1,12 +1,14 @@
#include <assert.h>
+#include <cutils/ashmem.h>
+#include <hwui/Canvas.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
#include <unistd.h>
-#include "jni.h"
-#include <nativehelper/JNIHelp.h>
#include "GraphicsJNI.h"
-
#include "SkBitmap.h"
#include "SkCanvas.h"
+#include "SkColor.h"
#include "SkColorSpace.h"
#include "SkFontMetrics.h"
#include "SkImageInfo.h"
@@ -14,10 +16,9 @@
#include "SkPoint.h"
#include "SkRect.h"
#include "SkRegion.h"
+#include "SkSamplingOptions.h"
#include "SkTypes.h"
-#include <cutils/ashmem.h>
-#include <hwui/Canvas.h>
-#include <log/log.h>
+#include "jni.h"
using namespace android;
@@ -630,13 +631,15 @@
////////////////////////////////////////////////////////////////////////////////
-RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
- bool mustMatchColorType)
+RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(
+ android::Bitmap* recycledBitmap, bool mustMatchColorType,
+ std::optional<SkRect> desiredSubset)
: mRecycledBitmap(recycledBitmap)
, mRecycledBytes(recycledBitmap ? recycledBitmap->getAllocationByteCount() : 0)
, mSkiaBitmap(nullptr)
, mNeedsCopy(false)
- , mMustMatchColorType(mustMatchColorType) {}
+ , mMustMatchColorType(mustMatchColorType)
+ , mDesiredSubset(getSourceBoundsForUpsample(desiredSubset)) {}
RecyclingClippingPixelAllocator::~RecyclingClippingPixelAllocator() {}
@@ -668,7 +671,8 @@
const SkImageInfo maxInfo = bitmap->info().makeWH(maxWidth, maxHeight);
const size_t rowBytes = maxInfo.minRowBytes();
const size_t bytesNeeded = maxInfo.computeByteSize(rowBytes);
- if (bytesNeeded <= mRecycledBytes) {
+
+ if (!mDesiredSubset && bytesNeeded <= mRecycledBytes) {
// Here we take advantage of reconfigure() to reset the rowBytes
// of mRecycledBitmap. It is very important that we pass in
// mRecycledBitmap->info() for the SkImageInfo. According to the
@@ -712,20 +716,31 @@
if (mNeedsCopy) {
mRecycledBitmap->ref();
android::Bitmap* recycledPixels = mRecycledBitmap;
- void* dst = recycledPixels->pixels();
- const size_t dstRowBytes = mRecycledBitmap->rowBytes();
- const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
- mSkiaBitmap->info().minRowBytes());
- const int rowsToCopy = std::min(mRecycledBitmap->info().height(),
- mSkiaBitmap->info().height());
- for (int y = 0; y < rowsToCopy; y++) {
- memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy);
- // Cast to bytes in order to apply the dstRowBytes offset correctly.
- dst = reinterpret_cast<void*>(
- reinterpret_cast<uint8_t*>(dst) + dstRowBytes);
+ if (mDesiredSubset) {
+ recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
+ recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
+
+ auto canvas = SkCanvas(recycledPixels->getSkBitmap());
+ SkRect destination = SkRect::Make(recycledPixels->info().bounds());
+ destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds()));
+ canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination,
+ SkSamplingOptions(SkFilterMode::kLinear), nullptr,
+ SkCanvas::kFast_SrcRectConstraint);
+ } else {
+ void* dst = recycledPixels->pixels();
+ const size_t dstRowBytes = mRecycledBitmap->rowBytes();
+ const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
+ mSkiaBitmap->info().minRowBytes());
+ const int rowsToCopy =
+ std::min(mRecycledBitmap->info().height(), mSkiaBitmap->info().height());
+ for (int y = 0; y < rowsToCopy; y++) {
+ memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy);
+ // Cast to bytes in order to apply the dstRowBytes offset correctly.
+ dst = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(dst) + dstRowBytes);
+ }
+ recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
+ recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
}
- recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
- recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
recycledPixels->notifyPixelsChanged();
recycledPixels->unref();
}
@@ -733,6 +748,20 @@
mSkiaBitmap = nullptr;
}
+std::optional<SkRect> RecyclingClippingPixelAllocator::getSourceBoundsForUpsample(
+ std::optional<SkRect> subset) {
+ if (!uirenderer::Properties::resampleGainmapRegions || !subset || subset->isEmpty()) {
+ return std::nullopt;
+ }
+
+ if (subset->left() == floor(subset->left()) && subset->top() == floor(subset->top()) &&
+ subset->right() == floor(subset->right()) && subset->bottom() == floor(subset->bottom())) {
+ return std::nullopt;
+ }
+
+ return subset;
+}
+
////////////////////////////////////////////////////////////////////////////////
AshmemPixelAllocator::AshmemPixelAllocator(JNIEnv *env) {
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index b0a1074..4b08f8d 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -216,8 +216,8 @@
*/
class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator {
public:
- RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
- bool mustMatchColorType = true);
+ RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, bool mustMatchColorType = true,
+ std::optional<SkRect> desiredSubset = std::nullopt);
~RecyclingClippingPixelAllocator();
@@ -241,11 +241,24 @@
SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kNo_ZeroInitialized; }
private:
+ /**
+ * Optionally returns a subset rectangle that we need to upsample from.
+ * E.g., a gainmap subset may be decoded in a slightly larger rectangle
+ * than is needed (in order to correctly preserve gainmap alignment when
+ * rendering at display time), so we need to re-sample the "intended"
+ * gainmap back up to the bitmap dimensions.
+ *
+ * If we don't need to upsample from a subregion, then returns an empty
+ * optional
+ */
+ static std::optional<SkRect> getSourceBoundsForUpsample(std::optional<SkRect> subset);
+
android::Bitmap* mRecycledBitmap;
const size_t mRecycledBytes;
SkBitmap* mSkiaBitmap;
bool mNeedsCopy;
const bool mMustMatchColorType;
+ const std::optional<SkRect> mDesiredSubset;
};
class AshmemPixelAllocator : public SkBitmap::Allocator {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
index e2ab316..c61a2ac 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -16,7 +16,9 @@
package com.android.packageinstaller.v2.ui
-import android.app.Activity
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_FIRST_USER
+import android.app.Activity.RESULT_OK
import android.app.AppOpsManager
import android.content.ActivityNotFoundException
import android.content.Intent
@@ -135,7 +137,7 @@
}
InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted)
- else -> setResult(Activity.RESULT_CANCELED, null, true)
+ else -> setResult(RESULT_CANCELED, null, true)
}
}
@@ -169,7 +171,7 @@
val success = installStage as InstallSuccess
if (success.shouldReturnResult) {
val successIntent = success.resultIntent
- setResult(Activity.RESULT_OK, successIntent, true)
+ setResult(RESULT_OK, successIntent, true)
} else {
val successDialog = InstallSuccessFragment(success)
showDialogInner(successDialog)
@@ -180,7 +182,7 @@
val failed = installStage as InstallFailed
if (failed.shouldReturnResult) {
val failureIntent = failed.resultIntent
- setResult(Activity.RESULT_FIRST_USER, failureIntent, true)
+ setResult(RESULT_FIRST_USER, failureIntent, true)
} else {
val failureDialog = InstallFailedFragment(failed)
showDialogInner(failureDialog)
@@ -219,7 +221,7 @@
shouldFinish = blockedByPolicyDialog == null
showDialogInner(blockedByPolicyDialog)
}
- setResult(Activity.RESULT_CANCELED, null, shouldFinish)
+ setResult(RESULT_CANCELED, null, shouldFinish)
}
/**
@@ -257,6 +259,10 @@
fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) {
super.setResult(resultCode, data)
+ if (resultCode != RESULT_OK) {
+ // Let callers know that the install was cancelled
+ installViewModel!!.cleanupInstall()
+ }
if (shouldFinish) {
finish()
}
@@ -282,7 +288,7 @@
if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) {
installViewModel!!.cleanupInstall()
}
- setResult(Activity.RESULT_CANCELED, null, true)
+ setResult(RESULT_CANCELED, null, true)
}
override fun onNegativeResponse(resultCode: Int, data: Intent?) {
@@ -318,7 +324,7 @@
if (localLogv) {
Log.d(LOG_TAG, "Opening $intent")
}
- setResult(Activity.RESULT_OK, intent, true)
+ setResult(RESULT_OK, intent, true)
if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
startActivity(intent)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index 7124ed2..c6eb9fd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -178,7 +178,7 @@
}
log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString());
- // Get the preferred main device by getPreferredMainDeviceWithoutConectionState
+ // Get the preferred main device by getPreferredMainDeviceWithoutConnectionState
List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId);
CachedBluetoothDevice preferredMainDevice =
getPreferredMainDevice(groupId, groupDevicesList);
@@ -373,6 +373,7 @@
preferredMainDevice.addMemberDevice(deviceItem);
mCachedDevices.remove(deviceItem);
mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
+ preferredMainDevice.refresh();
hasChanged = true;
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 27fcdbe..26905b1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -80,6 +80,7 @@
"com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE";
public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE";
public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE";
+ public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING";
public static final int BROADCAST_STATE_UNKNOWN = 0;
public static final int BROADCAST_STATE_ON = 1;
public static final int BROADCAST_STATE_OFF = 2;
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/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
index f94f21f..698eb81 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
@@ -19,7 +19,9 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothClass;
@@ -352,4 +354,34 @@
assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3);
assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
}
+
+ @Test
+ public void onProfileConnectionStateChangedIfProcessed_addMemberDevice_refreshUI() {
+ mCachedDevice3.setGroupId(GROUP1);
+
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3,
+ BluetoothProfile.STATE_CONNECTED);
+
+ verify(mCachedDevice1).refresh();
+ }
+
+ @Test
+ public void onProfileConnectionStateChangedIfProcessed_switchMainDevice_refreshUI() {
+ when(mDevice3.isConnected()).thenReturn(true);
+ when(mDevice2.isConnected()).thenReturn(false);
+ when(mDevice1.isConnected()).thenReturn(false);
+ mCachedDevice3.setGroupId(GROUP1);
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3,
+ BluetoothProfile.STATE_CONNECTED);
+
+ when(mDevice3.isConnected()).thenReturn(false);
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3,
+ BluetoothProfile.STATE_DISCONNECTED);
+ when(mDevice1.isConnected()).thenReturn(true);
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1,
+ BluetoothProfile.STATE_CONNECTED);
+
+ verify(mCachedDevice3).switchMemberDeviceContent(mCachedDevice1);
+ verify(mCachedDevice3, atLeastOnce()).refresh();
+ }
}
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/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..1ff9a80 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"
@@ -1274,6 +1263,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/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt
new file mode 100644
index 0000000..8f5cdbf
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Checks if registerContentObserver/registerContentObserverAsUser/unregisterContentObserver is
+ * called on a ContentResolver (or subclasses), and directs the caller to using
+ * com.android.systemui.util.settings.SettingsProxy or its sub-classes.
+ */
+@Suppress("UnstableApiUsage")
+class RegisterContentObserverViaContentResolverDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return CONTENT_RESOLVER_METHOD_LIST
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val classQualifiedName = node.getParentOfType(UClass::class.java)?.qualifiedName
+ if (classQualifiedName in CLASSNAME_ALLOWLIST) {
+ // Don't warn for class we want the developers to use.
+ return
+ }
+
+ val evaluator = context.evaluator
+ if (evaluator.isMemberInSubClassOf(method, "android.content.ContentResolver")) {
+ context.report(
+ issue = CONTENT_RESOLVER_ERROR,
+ location = context.getNameLocation(node),
+ message =
+ "`ContentResolver.${method.name}()` should be replaced with " +
+ "an appropriate interface API call, for eg. " +
+ "`<SettingsProxy>/<UserSettingsProxy>.${method.name}()`"
+ )
+ }
+ }
+
+ companion object {
+ @JvmField
+ val CONTENT_RESOLVER_ERROR: Issue =
+ Issue.create(
+ id = "RegisterContentObserverViaContentResolver",
+ briefDescription =
+ "Content observer registration done via `ContentResolver`" +
+ "instead of `SettingsProxy or child interfaces.`",
+ // lint trims indents and converts \ to line continuations
+ explanation =
+ """
+ Use registerContentObserver/unregisterContentObserver methods in \
+ `SettingsProxy`, `UserSettingsProxy` or `GlobalSettings` class instead of \
+ using `ContentResolver.registerContentObserver` or \
+ `ContentResolver.unregisterContentObserver`.""",
+ category = Category.PERFORMANCE,
+ priority = 10,
+ severity = Severity.ERROR,
+ implementation =
+ Implementation(
+ RegisterContentObserverViaContentResolverDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ private val CLASSNAME_ALLOWLIST =
+ listOf(
+ "com.android.systemui.util.settings.SettingsProxy",
+ "com.android.systemui.util.settings.UserSettingsProxy",
+ "com.android.systemui.util.settings.GlobalSettings",
+ "com.android.systemui.util.settings.SecureSettings",
+ "com.android.systemui.util.settings.SystemSettings"
+ )
+
+ private val CONTENT_RESOLVER_METHOD_LIST =
+ listOf(
+ "registerContentObserver",
+ "registerContentObserverAsUser",
+ "unregisterContentObserver"
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 5206b05..a1f4f55 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -46,7 +46,8 @@
DemotingTestWithoutBugDetector.ISSUE,
TestFunctionNameViolationDetector.ISSUE,
MissingApacheLicenseDetector.ISSUE,
- RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING
+ RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING,
+ RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR
)
override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt
new file mode 100644
index 0000000..1d33bce
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class RegisterContentObserverViaContentResolverDetectorTest : SystemUILintDetectorTest() {
+
+ override fun getDetector(): Detector = RegisterContentObserverViaContentResolverDetector()
+
+ override fun getIssues(): List<Issue> =
+ listOf(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+
+ @Test
+ fun testRegisterContentObserver_throwError() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserver() [RegisterContentObserverViaContentResolver]
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testRegisterContentObserverForUser_throwError() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserverAsUser() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserverAsUser() [RegisterContentObserverViaContentResolver]
+ registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testSuppressRegisterContentObserver() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ @SuppressWarnings("RegisterContentObserverViaContentResolver")
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testRegisterContentObserverInSettingsProxy_allowed() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package com.android.systemui.util.settings;
+ import android.content.Context;
+
+ public class SettingsProxy {
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testNoopIfNoCall() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class SettingsProxy {
+ public void register(Context context) {
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testUnRegisterContentObserver_throwError() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void register(Context context) {
+ context.getContentResolver().
+ unregisterContentObserver(mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Error: ContentResolver.unregisterContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver() [RegisterContentObserverViaContentResolver]
+ unregisterContentObserver(mSettingObserver);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+}
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/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
index 9afb4d5..a78c038 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -17,7 +17,6 @@
package com.android.systemui.keyguard.ui.composable
import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule
-import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule
import dagger.Module
@@ -26,7 +25,6 @@
[
CommunalBlueprintModule::class,
OptionalSectionModule::class,
- ShortcutsBesideUdfpsBlueprintModule::class,
],
)
interface LockscreenSceneBlueprintModule
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
deleted file mode 100644
index a5e120c..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ /dev/null
@@ -1,259 +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.keyguard.ui.composable.blueprint
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-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.graphicsLayer
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.unit.IntRect
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.compose.animation.scene.SceneScope
-import com.android.compose.modifiers.padding
-import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
-import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
-import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
-import com.android.systemui.keyguard.ui.composable.section.LockSection
-import com.android.systemui.keyguard.ui.composable.section.NotificationSection
-import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
-import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
-import com.android.systemui.keyguard.ui.composable.section.TopAreaSection
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.IntoSet
-import java.util.Optional
-import javax.inject.Inject
-import kotlin.math.roundToInt
-
-/**
- * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
- * factor).
- */
-class ShortcutsBesideUdfpsBlueprint
-@Inject
-constructor(
- private val statusBarSection: StatusBarSection,
- private val lockSection: LockSection,
- private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
- private val bottomAreaSection: BottomAreaSection,
- private val settingsMenuSection: SettingsMenuSection,
- private val topAreaSection: TopAreaSection,
- private val notificationSection: NotificationSection,
-) : ComposableLockscreenSceneBlueprint {
-
- override val id: String = "shortcuts-besides-udfps"
-
- @Composable
- override fun SceneScope.Content(
- viewModel: LockscreenContentViewModel,
- modifier: Modifier,
- ) {
- val isUdfpsVisible = viewModel.isUdfpsVisible
- val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle()
- val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle()
- val areNotificationsVisible by
- viewModel
- .areNotificationsVisible(contentKey)
- .collectAsStateWithLifecycle(initialValue = false)
-
- LockscreenLongPress(
- viewModel = viewModel.touchHandling,
- modifier = modifier,
- ) { onSettingsMenuPlaced ->
- Layout(
- content = {
- // Constrained to above the lock icon.
- Column(
- modifier = Modifier.fillMaxSize(),
- ) {
- with(statusBarSection) {
- StatusBar(
- modifier =
- Modifier.fillMaxWidth()
- .padding(
- horizontal = { unfoldTranslations.start.roundToInt() },
- )
- )
- }
-
- Box {
- with(topAreaSection) {
- DefaultClockLayout(
- smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop,
- modifier =
- Modifier.graphicsLayer {
- translationX = unfoldTranslations.start
- },
- )
- }
- if (isShadeLayoutWide) {
- with(notificationSection) {
- Notifications(
- areNotificationsVisible = areNotificationsVisible,
- isShadeLayoutWide = isShadeLayoutWide,
- burnInParams = null,
- modifier =
- Modifier.fillMaxWidth(0.5f)
- .fillMaxHeight()
- .align(alignment = Alignment.TopEnd)
- )
- }
- }
- }
- if (!isShadeLayoutWide) {
- with(notificationSection) {
- Notifications(
- areNotificationsVisible = areNotificationsVisible,
- isShadeLayoutWide = isShadeLayoutWide,
- burnInParams = null,
- modifier = Modifier.weight(weight = 1f)
- )
- }
- }
- if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
- with(ambientIndicationSectionOptional.get()) {
- AmbientIndication(modifier = Modifier.fillMaxWidth())
- }
- }
- }
-
- // Constrained to the left of the lock icon (in left-to-right layouts).
- with(bottomAreaSection) {
- Shortcut(
- isStart = true,
- applyPadding = false,
- modifier =
- Modifier.graphicsLayer { translationX = unfoldTranslations.start },
- )
- }
-
- with(lockSection) { LockIcon() }
-
- // Constrained to the right of the lock icon (in left-to-right layouts).
- with(bottomAreaSection) {
- Shortcut(
- isStart = false,
- applyPadding = false,
- modifier =
- Modifier.graphicsLayer { translationX = unfoldTranslations.end },
- )
- }
-
- // Aligned to bottom and constrained to below the lock icon.
- Column(modifier = Modifier.fillMaxWidth()) {
- if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
- with(ambientIndicationSectionOptional.get()) {
- AmbientIndication(modifier = Modifier.fillMaxWidth())
- }
- }
-
- with(bottomAreaSection) {
- IndicationArea(modifier = Modifier.fillMaxWidth())
- }
- }
-
- // Aligned to bottom and NOT constrained by the lock icon.
- with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
- },
- modifier = Modifier.fillMaxSize(),
- ) { measurables, constraints ->
- check(measurables.size == 6)
- val aboveLockIconMeasurable = measurables[0]
- val startSideShortcutMeasurable = measurables[1]
- val lockIconMeasurable = measurables[2]
- val endSideShortcutMeasurable = measurables[3]
- val belowLockIconMeasurable = measurables[4]
- val settingsMenuMeasurable = measurables[5]
-
- val noMinConstraints =
- constraints.copy(
- minWidth = 0,
- minHeight = 0,
- )
-
- val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
- val lockIconBounds =
- IntRect(
- left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
- top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
- right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
- bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
- )
-
- val aboveLockIconPlaceable =
- aboveLockIconMeasurable.measure(
- noMinConstraints.copy(maxHeight = lockIconBounds.top)
- )
- val startSideShortcutPlaceable =
- startSideShortcutMeasurable.measure(noMinConstraints)
- val endSideShortcutPlaceable = endSideShortcutMeasurable.measure(noMinConstraints)
- val belowLockIconPlaceable =
- belowLockIconMeasurable.measure(
- noMinConstraints.copy(
- maxHeight = constraints.maxHeight - lockIconBounds.bottom
- )
- )
- val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints)
-
- layout(constraints.maxWidth, constraints.maxHeight) {
- aboveLockIconPlaceable.place(
- x = 0,
- y = 0,
- )
- startSideShortcutPlaceable.placeRelative(
- x = lockIconBounds.left / 2 - startSideShortcutPlaceable.width / 2,
- y = lockIconBounds.center.y - startSideShortcutPlaceable.height / 2,
- )
- lockIconPlaceable.place(
- x = lockIconBounds.left,
- y = lockIconBounds.top,
- )
- endSideShortcutPlaceable.placeRelative(
- x =
- lockIconBounds.right +
- (constraints.maxWidth - lockIconBounds.right) / 2 -
- endSideShortcutPlaceable.width / 2,
- y = lockIconBounds.center.y - endSideShortcutPlaceable.height / 2,
- )
- belowLockIconPlaceable.place(
- x = 0,
- y = constraints.maxHeight - belowLockIconPlaceable.height,
- )
- settingsMenuPlaceable.place(
- x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
- y = constraints.maxHeight - settingsMenuPlaceable.height,
- )
- }
- }
- }
- }
-}
-
-@Module
-interface ShortcutsBesideUdfpsBlueprintModule {
- @Binds
- @IntoSet
- fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): ComposableLockscreenSceneBlueprint
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
index 9c72d93..364adca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
@@ -32,7 +32,6 @@
import androidx.core.content.res.ResourcesCompat
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
@@ -40,10 +39,8 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
-import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.Flow
@@ -52,11 +49,9 @@
@Inject
constructor(
private val viewModel: KeyguardQuickAffordancesCombinedViewModel,
- private val falsingManager: FalsingManager,
- private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val indicationAreaViewModel: KeyguardIndicationAreaViewModel,
- private val shortcutsLogger: KeyguardQuickAffordancesLogger,
+ private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
) {
/**
* Renders a single lockscreen shortcut.
@@ -80,9 +75,8 @@
viewId = if (isStart) R.id.start_button else R.id.end_button,
viewModel = if (isStart) viewModel.startButton else viewModel.endButton,
transitionAlpha = viewModel.transitionAlpha,
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
indicationController = indicationController,
+ binder = keyguardQuickAffordanceViewBinder,
modifier =
if (applyPadding) {
Modifier.shortcutPadding()
@@ -124,9 +118,8 @@
@IdRes viewId: Int,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
transitionAlpha: Flow<Float>,
- falsingManager: FalsingManager,
- vibratorHelper: VibratorHelper,
indicationController: KeyguardIndicationController,
+ binder: KeyguardQuickAffordanceViewBinder,
modifier: Modifier = Modifier,
) {
val (binding, setBinding) = mutableStateOf<KeyguardQuickAffordanceViewBinder.Binding?>(null)
@@ -158,13 +151,10 @@
}
setBinding(
- KeyguardQuickAffordanceViewBinder.bind(
+ binder.bind(
view,
viewModel,
transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
) {
indicationController.showTransientIndication(it)
}
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/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index 0105af3..fe16ef751 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -127,16 +127,7 @@
return coroutineScope
.launch(start = CoroutineStart.ATOMIC) {
try {
- if (currentScene == toScene) {
- animatable.animateTo(targetProgress, transformationSpec.progressSpec)
- } else {
- // If the back gesture is cancelled, the progress is animated back to 0f by
- // the system. But we need this animate call anyways because
- // PredictiveBackHandler doesn't guarantee that it ends at 0f. Since the
- // remaining change in progress is usually very small, the progressSpec is
- // omitted and the default spring spec used instead.
- animatable.animateTo(targetProgress)
- }
+ animatable.animateTo(targetProgress)
} finally {
state.finishTransition(this@PredictiveBackTransition, scene)
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index c414fbe..0eaecb0 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -18,8 +18,6 @@
import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.rememberCoroutineScope
@@ -61,23 +59,7 @@
@Test
fun testPredictiveBack() {
- val transitionFrames = 2
- val layoutState =
- rule.runOnUiThread {
- MutableSceneTransitionLayoutState(
- SceneA,
- transitions =
- transitions {
- from(SceneA, to = SceneB) {
- spec =
- tween(
- durationMillis = transitionFrames * 16,
- easing = LinearEasing
- )
- }
- }
- )
- }
+ val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
rule.setContent {
SceneTransitionLayout(layoutState) {
scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
@@ -106,27 +88,12 @@
assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
assertThat(layoutState.transitionState).isIdle()
- rule.mainClock.autoAdvance = false
-
// Start again and commit it.
rule.runOnUiThread {
dispatcher.dispatchOnBackStarted(backEvent())
dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
dispatcher.onBackPressed()
}
- rule.mainClock.advanceTimeByFrame()
- rule.waitForIdle()
- val transition2 = assertThat(layoutState.transitionState).isTransition()
- // verify that transition picks up progress from preview
- assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f)
-
- rule.mainClock.advanceTimeByFrame()
- rule.waitForIdle()
- // verify that transition is half way between preview-end-state (0.4f) and target-state (1f)
- // after one frame
- assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f)
-
- rule.mainClock.autoAdvance = true
rule.waitForIdle()
assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
assertThat(layoutState.transitionState).isIdle()
diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml
index b4c839f..7577147 100644
--- a/packages/SystemUI/lint-baseline.xml
+++ b/packages/SystemUI/lint-baseline.xml
@@ -32157,4 +32157,631 @@
column="6"/>
</issue>
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt"
+ line="154"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(ALWAYS_ON_DISPLAY_CONSTANTS_URI,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java"
+ line="164"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt"
+ line="61"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" awaitClose { resolver.unregisterContentObserver(observer) }"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt"
+ line="67"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mSettingObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="123"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mSettingObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="180"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="211"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="219"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="46"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="48"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" resolver.unregisterContentObserver(allowedObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="54"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" resolver.unregisterContentObserver(onObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="55"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(mQuickPickupGesture, false, this,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java"
+ line="511"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java"
+ line="513"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(mAlwaysOnEnabled, false, this,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java"
+ line="514"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="2470"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3077"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3168"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3957"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3961"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt"
+ line="486"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt"
+ line="492"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" contentResolver.unregisterContentObserver(settingsObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt"
+ line="543"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt"
+ line="82"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" contentResolver.unregisterContentObserver(settingsObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt"
+ line="100"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java"
+ line="275"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java"
+ line="276"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java"
+ line="277"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(Global.getUriFor(Global.MOBILE_DATA),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java"
+ line="200"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(Global.getUriFor("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java"
+ line="202"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java"
+ line="212"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" context.contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NaturalScrollingSettingObserver.kt"
+ line="54"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="253"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="256"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="259"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="262"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mAssistContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="295"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java"
+ line="395"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java"
+ line="400"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java"
+ line="3675"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mSettingsChangeObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java"
+ line="4705"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(Settings.Global.getUriFor("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java"
+ line="191"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java"
+ line="205"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java"
+ line="216"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java"
+ line="178"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeveloperSettingsObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java"
+ line="186"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java"
+ line="83"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java"
+ line="100"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java"
+ line="219"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java"
+ line="241"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java"
+ line="243"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java"
+ line="1235"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java"
+ line="1236"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java"
+ line="1240"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mResolver.unregisterContentObserver(this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java"
+ line="377"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java"
+ line="379"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java"
+ line="381"
+ column="23"/>
+ </issue>
+
</issues>
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/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
new file mode 100644
index 0000000..1f73347
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 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.education.domain.ui.view
+
+import android.content.applicationContext
+import android.widget.Toast
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
+import com.android.systemui.education.domain.interactor.contextualEducationInteractor
+import com.android.systemui.education.domain.interactor.keyboardTouchpadEduInteractor
+import com.android.systemui.education.ui.view.ContextualEduUiCoordinator
+import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ContextualEduUiCoordinatorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val interactor = kosmos.contextualEducationInteractor
+ private lateinit var underTest: ContextualEduUiCoordinator
+ @Mock private lateinit var toast: Toast
+
+ @get:Rule val mockitoRule = MockitoJUnit.rule()
+
+ @Before
+ fun setUp() {
+ val viewModel =
+ ContextualEduViewModel(
+ kosmos.applicationContext.resources,
+ kosmos.keyboardTouchpadEduInteractor
+ )
+ underTest =
+ ContextualEduUiCoordinator(kosmos.applicationCoroutineScope, viewModel) { _ -> toast }
+ underTest.start()
+ kosmos.keyboardTouchpadEduInteractor.start()
+ }
+
+ @Test
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ fun showToastOnNewEdu() =
+ testScope.runTest {
+ triggerEducation(BACK)
+ runCurrent()
+ verify(toast).show()
+ }
+
+ private suspend fun triggerEducation(gestureType: GestureType) {
+ for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+ interactor.incrementSignalCount(gestureType)
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index dc225a3..638c957 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -16,8 +16,6 @@
package com.android.systemui.keyguard.domain.interactor
-import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
-import com.android.systemui.coroutines.collectLastValue
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,18 +26,16 @@
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
import com.android.systemui.kosmos.testScope
-import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -49,7 +45,6 @@
import org.junit.runner.RunWith
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
-import com.google.common.truth.Truth.assertThat
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 90e13a5..8c1e8de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -757,6 +757,30 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
+ fun goneToOccluded() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GONE
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+
+ // WHEN an occluding app is running and showDismissibleKeyguard is called
+ keyguardRepository.setKeyguardOccluded(true)
+ keyguardRepository.showDismissibleKeyguard()
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.GONE,
+ to = KeyguardState.OCCLUDED,
+ ownerName =
+ "FromGoneTransitionInteractor" + "(Dismissible keyguard with occlusion)",
+ animatorAssertion = { it.isNotNull() }
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
@DisableSceneContainer
fun goneToDreaming() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
index 3f6e229..df8afdb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -23,6 +23,7 @@
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -117,6 +118,24 @@
}
@Test
+ fun lockscreenAlphaStartsFromViewStateAccessorAlpha() =
+ testScope.runTest {
+ val viewState = ViewStateAccessor(alpha = { 0.5f })
+ val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+
+ keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+ keyguardTransitionRepository.sendTransitionStep(step(0f))
+ assertThat(alpha).isEqualTo(0.5f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(0.5f))
+ assertThat(alpha).isEqualTo(0.75f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(1f))
+ assertThat(alpha).isEqualTo(1f)
+ }
+
+ @Test
fun deviceEntryBackgroundViewDisappear() =
testScope.runTest {
val values by collectValues(underTest.deviceEntryBackgroundViewAlpha)
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/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 8a43198..fadb1d7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -22,13 +22,13 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.kosmos.testScope
-import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
@@ -43,6 +43,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
class ShadeInteractorSceneContainerImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
@@ -50,7 +51,7 @@
private val configurationRepository = kosmos.fakeConfigurationRepository
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val sceneInteractor = kosmos.sceneInteractor
- private val shadeRepository = kosmos.shadeRepository
+ private val shadeTestUtil = kosmos.shadeTestUtil
private val underTest = kosmos.shadeInteractorSceneContainerImpl
@@ -60,7 +61,7 @@
val actual by collectLastValue(underTest.qsExpansion)
// WHEN split shade is enabled and QS is expanded
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val transitionState =
@@ -89,7 +90,7 @@
// WHEN split shade is not enabled and QS is expanded
keyguardRepository.setStatusBarState(StatusBarState.SHADE)
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val progress = MutableStateFlow(.3f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 2fb9e1e0..733cac9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -56,6 +56,7 @@
import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.mockLargeScreenHeaderHelper
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
@@ -135,11 +136,14 @@
val communalSceneRepository
get() = kosmos.communalSceneRepository
+ val shadeRepository
+ get() = kosmos.fakeShadeRepository
+
lateinit var underTest: SharedNotificationContainerViewModel
@Before
fun setUp() {
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
movementFlow = MutableStateFlow(BurnInModel())
whenever(aodBurnInViewModel.movement(any())).thenReturn(movementFlow)
underTest = kosmos.sharedNotificationContainerViewModel
@@ -148,7 +152,7 @@
@Test
fun validateMarginStartInSplitShade() =
testScope.runTest {
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
val dimens by collectLastValue(underTest.configurationBasedDimensions)
@@ -161,7 +165,7 @@
@Test
fun validateMarginStart() =
testScope.runTest {
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
val dimens by collectLastValue(underTest.configurationBasedDimensions)
@@ -175,7 +179,7 @@
fun validatePaddingTopInSplitShade_usesLargeHeaderHelper() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -191,7 +195,7 @@
fun validatePaddingTopInNonSplitShade_usesLargeScreenHeader() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -207,7 +211,7 @@
fun validatePaddingTopInNonSplitShade_doesNotUseLargeScreenHeader() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
overrideResource(R.bool.config_use_large_screen_shade_header, false)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -508,7 +512,7 @@
val bounds by collectLastValue(underTest.bounds)
// When not in split shade
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
runCurrent()
@@ -567,7 +571,7 @@
// When in split shade
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -628,7 +632,7 @@
advanceTimeBy(50L)
showLockscreen()
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
assertThat(maxNotifications).isEqualTo(10)
@@ -656,7 +660,7 @@
advanceTimeBy(50L)
showLockscreen()
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
assertThat(maxNotifications).isEqualTo(10)
@@ -690,7 +694,7 @@
// Show lockscreen with shade expanded
showLockscreenWithShadeExpanded()
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
// -1 means No Limit
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/drawable/ic_bugreport.xml b/packages/SystemUI/res/drawable/ic_bugreport.xml
index ed1c6c7..badbd88 100644
--- a/packages/SystemUI/res/drawable/ic_bugreport.xml
+++ b/packages/SystemUI/res/drawable/ic_bugreport.xml
@@ -19,14 +19,14 @@
android:height="24.0dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
- android:tint="?attr/colorControlNormal">
+ android:tint="?android:attr/colorControlNormal">
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M20,10V8h-2.81c-0.45,-0.78 -1.07,-1.46 -1.82,-1.96L17,4.41L15.59,3l-2.17,2.17c-0.03,-0.01 -0.05,-0.01 -0.08,-0.01c-0.16,-0.04 -0.32,-0.06 -0.49,-0.09c-0.06,-0.01 -0.11,-0.02 -0.17,-0.03C12.46,5.02 12.23,5 12,5h0c-0.49,0 -0.97,0.07 -1.42,0.18l0.02,-0.01L8.41,3L7,4.41l1.62,1.63l0.01,0C7.88,6.54 7.26,7.22 6.81,8H4v2h2.09C6.03,10.33 6,10.66 6,11v1H4v2h2v1c0,0.34 0.04,0.67 0.09,1H4v2h2.81c1.04,1.79 2.97,3 5.19,3h0c2.22,0 4.15,-1.21 5.19,-3H20v-2h-2.09l0,0c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1l0,0H20zM16,15c0,2.21 -1.79,4 -4,4c-2.21,0 -4,-1.79 -4,-4v-4c0,-2.21 1.79,-4 4,-4h0c2.21,0 4,1.79 4,4V15z"/>
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M10,14h4v2h-4z"/>
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M10,10h4v2h-4z"/>
</vector>
\ No newline at end of file
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 8751596..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>
@@ -971,8 +975,8 @@
<string name="hearing_devices_presets_error">Couldn\'t update preset</string>
<!-- QuickSettings: Title for hearing aids presets. Preset is a set of hearing aid settings. User can apply different settings in different environments (e.g. Outdoor, Restaurant, Home) [CHAR LIMIT=40]-->
<string name="hearing_devices_preset_label">Preset</string>
- <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40]-->
- <string name="live_caption_title">Live Caption</string>
+ <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40] [BACKUP_MESSAGE_ID=8916875614623730005]-->
+ <string name="quick_settings_hearing_devices_live_caption_title">Live Caption</string>
<!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
<string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</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/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/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 083f1db..d08653c3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -228,7 +228,7 @@
mHearingDeviceItemList = getHearingDevicesList();
if (mPresetsController != null) {
activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList);
- mPresetsController.setActiveHearingDevice(activeHearingDevice);
+ mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
} else {
activeHearingDevice = null;
}
@@ -336,7 +336,7 @@
}
final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
mHearingDeviceItemList);
- mPresetsController.setActiveHearingDevice(activeHearingDevice);
+ mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
R.layout.hearing_devices_preset_spinner_selected,
@@ -499,7 +499,8 @@
final List<ResolveInfo> resolved = packageManager.queryIntentActivities(LIVE_CAPTION_INTENT,
/* flags= */ 0);
if (!resolved.isEmpty()) {
- return new ToolItem(context.getString(R.string.live_caption_title),
+ return new ToolItem(
+ context.getString(R.string.quick_settings_hearing_devices_live_caption_title),
context.getDrawable(R.drawable.ic_volume_odi_captions),
LIVE_CAPTION_INTENT);
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
index b46b8fe..664f3f8 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.settingslib.Utils;
import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.res.R;
@@ -105,6 +106,7 @@
private final TextView mNameView;
private final TextView mSummaryView;
private final ImageView mIconView;
+ private final ImageView mGearIcon;
private final View mGearView;
DeviceItemViewHolder(@NonNull View itemView, Context context) {
@@ -114,6 +116,7 @@
mNameView = itemView.requireViewById(R.id.bluetooth_device_name);
mSummaryView = itemView.requireViewById(R.id.bluetooth_device_summary);
mIconView = itemView.requireViewById(R.id.bluetooth_device_icon);
+ mGearIcon = itemView.requireViewById(R.id.gear_icon_image);
mGearView = itemView.requireViewById(R.id.gear_icon);
}
@@ -124,13 +127,31 @@
if (backgroundResId != null) {
mContainer.setBackground(mContext.getDrawable(item.getBackground()));
}
- mNameView.setText(item.getDeviceName());
- mSummaryView.setText(item.getConnectionSummary());
+
+ // tint different color in different state for bad color contrast problem
+ int tintColor = item.isActive() ? Utils.getColorAttr(mContext,
+ com.android.internal.R.attr.materialColorOnPrimaryContainer).getDefaultColor()
+ : Utils.getColorAttr(mContext,
+ com.android.internal.R.attr.materialColorOnSurface).getDefaultColor();
+
Pair<Drawable, String> iconPair = item.getIconWithDescription();
if (iconPair != null) {
- mIconView.setImageDrawable(iconPair.getFirst());
+ Drawable drawable = iconPair.getFirst().mutate();
+ drawable.setTint(tintColor);
+ mIconView.setImageDrawable(drawable);
mIconView.setContentDescription(iconPair.getSecond());
}
+
+ mNameView.setTextAppearance(
+ item.isActive() ? R.style.BluetoothTileDialog_DeviceName_Active
+ : R.style.BluetoothTileDialog_DeviceName);
+ mNameView.setText(item.getDeviceName());
+ mSummaryView.setTextAppearance(
+ item.isActive() ? R.style.BluetoothTileDialog_DeviceSummary_Active
+ : R.style.BluetoothTileDialog_DeviceSummary);
+ mSummaryView.setText(item.getConnectionSummary());
+
+ mGearIcon.getDrawable().mutate().setTint(tintColor);
mGearView.setOnClickListener(view -> callback.onDeviceItemGearClicked(item, view));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
index f81124e..aa95fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
@@ -113,7 +113,7 @@
@Override
public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
@@ -137,7 +137,7 @@
@Override
public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
@@ -177,22 +177,33 @@
}
/**
- * Sets the hearing device for this controller to control the preset.
+ * Sets the hearing device for this controller to control the preset if it supports
+ * {@link HapClientProfile}.
*
* @param activeHearingDevice the {@link CachedBluetoothDevice} need to be hearing aid device
+ * and support {@link HapClientProfile}.
*/
- public void setActiveHearingDevice(CachedBluetoothDevice activeHearingDevice) {
- mActiveHearingDevice = activeHearingDevice;
+ public void setHearingDeviceIfSupportHap(CachedBluetoothDevice activeHearingDevice) {
+ if (mHapClientProfile == null || activeHearingDevice == null) {
+ mActiveHearingDevice = null;
+ return;
+ }
+ if (activeHearingDevice.getProfiles().stream().anyMatch(
+ profile -> profile instanceof HapClientProfile)) {
+ mActiveHearingDevice = activeHearingDevice;
+ } else {
+ mActiveHearingDevice = null;
+ }
}
/**
* Selects the currently active preset for {@code mActiveHearingDevice} individual device or
- * the device group accoridng to whether it supports synchronized presets or not.
+ * the device group according to whether it supports synchronized presets or not.
*
* @param presetIndex an index of one of the available presets
*/
public void selectPreset(int presetIndex) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
mSelectedPresetIndex = presetIndex;
@@ -217,7 +228,7 @@
* @return a list of all known preset info
*/
public List<BluetoothHapPresetInfo> getAllPresetInfo() {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return emptyList();
}
return mHapClientProfile.getAllPresetInfo(mActiveHearingDevice.getDevice()).stream().filter(
@@ -230,14 +241,14 @@
* @return active preset index
*/
public int getActivePresetIndex() {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
}
return mHapClientProfile.getActivePresetIndex(mActiveHearingDevice.getDevice());
}
private void selectPresetSynchronously(int groupId, int presetIndex) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (DEBUG) {
@@ -250,7 +261,7 @@
}
private void selectPresetIndependently(int presetIndex) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (DEBUG) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 9521be1..aed3c10 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);
@@ -878,7 +657,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..736400e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -173,7 +173,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;
@@ -728,7 +727,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 +777,6 @@
});
mWakefulnessLifecycle = wakefulnessLifecycle;
- mPanelInteractionDetector = panelInteractionDetector;
mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null;
@@ -1229,7 +1226,6 @@
operationId,
requestId,
mWakefulnessLifecycle,
- mPanelInteractionDetector,
mUserManager,
mLockPatternUtils,
viewModel);
@@ -1306,7 +1302,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 +1318,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/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/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..fa3230e1 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
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..865035e 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 =
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/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index 096556f..7e2c9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -26,6 +26,7 @@
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
+import com.android.systemui.education.ui.view.ContextualEduUiCoordinator
import dagger.Binds
import dagger.Lazy
import dagger.Module
@@ -74,7 +75,7 @@
implLazy.get()
} else {
// No-op implementation when the flag is disabled.
- return NoOpContextualEducationInteractor
+ return NoOpCoreStartable
}
}
@@ -91,6 +92,8 @@
}
@Provides
+ @IntoMap
+ @ClassKey(KeyboardTouchpadEduInteractor::class)
fun provideKeyboardTouchpadEduInteractor(
implLazy: Lazy<KeyboardTouchpadEduInteractor>
): CoreStartable {
@@ -98,22 +101,32 @@
implLazy.get()
} else {
// No-op implementation when the flag is disabled.
- return NoOpKeyboardTouchpadEduInteractor
+ return NoOpCoreStartable
+ }
+ }
+
+ @Provides
+ @IntoMap
+ @ClassKey(ContextualEduUiCoordinator::class)
+ fun provideContextualEduUiCoordinator(
+ implLazy: Lazy<ContextualEduUiCoordinator>
+ ): CoreStartable {
+ return if (Flags.keyboardTouchpadContextualEducation()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ return NoOpCoreStartable
}
}
}
+}
- private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
- override fun incrementSignalCount(gestureType: GestureType) {}
+private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
+ override fun incrementSignalCount(gestureType: GestureType) {}
- override fun updateShortcutTriggerTime(gestureType: GestureType) {}
- }
+ override fun updateShortcutTriggerTime(gestureType: GestureType) {}
+}
- private object NoOpContextualEducationInteractor : CoreStartable {
- override fun start() {}
- }
-
- private object NoOpKeyboardTouchpadEduInteractor : CoreStartable {
- override fun start() {}
- }
+private object NoOpCoreStartable : CoreStartable {
+ override fun start() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
new file mode 100644
index 0000000..b446ea2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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.education.ui.view
+
+import android.content.Context
+import android.widget.Toast
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.education.ui.viewmodel.ContextualEduContentViewModel
+import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * A class to show contextual education on UI based on the edu produced from
+ * [ContextualEduViewModel]
+ */
+@SysUISingleton
+class ContextualEduUiCoordinator
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val viewModel: ContextualEduViewModel,
+ private val createToast: (String) -> Toast
+) : CoreStartable {
+
+ @Inject
+ constructor(
+ @Application applicationScope: CoroutineScope,
+ context: Context,
+ viewModel: ContextualEduViewModel,
+ ) : this(
+ applicationScope,
+ viewModel,
+ createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) }
+ )
+
+ override fun start() {
+ applicationScope.launch {
+ viewModel.eduContent.collect { contentModel ->
+ if (contentModel.type == EducationUiType.Toast) {
+ showToast(contentModel)
+ }
+ }
+ }
+ }
+
+ private fun showToast(model: ContextualEduContentViewModel) {
+ val toast = createToast(model.message)
+ toast.show()
+ }
+}
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt
similarity index 65%
copy from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
copy to packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt
index 460de8c..3cba4c8 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright 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.
@@ -14,18 +14,8 @@
* limitations under the License.
*/
-package android.telephony.satellite.stub;
+package com.android.systemui.education.ui.viewmodel
-/**
- * {@hide}
- */
-parcelable ProvisionSubscriberId {
- /** provision subscriberId */
- String subscriberId;
+import com.android.systemui.education.shared.model.EducationUiType
- /** carrier id */
- int mCarrierId;
-
- /** apn */
- String mNiddApn;
-}
+data class ContextualEduContentViewModel(val message: String, val type: EducationUiType)
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
new file mode 100644
index 0000000..58276e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 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.education.ui.viewmodel
+
+import android.content.res.Resources
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
+import com.android.systemui.education.shared.model.EducationInfo
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class ContextualEduViewModel
+@Inject
+constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) {
+ val eduContent: Flow<ContextualEduContentViewModel> =
+ interactor.educationTriggered.filterNotNull().map {
+ ContextualEduContentViewModel(getEduContent(it), it.educationUiType)
+ }
+
+ private fun getEduContent(educationInfo: EducationInfo): String {
+ // Todo: also check UiType in educationInfo to determine the string
+ val resourceId =
+ when (educationInfo.gestureType) {
+ GestureType.BACK -> R.string.back_edu_toast_content
+ GestureType.HOME -> R.string.home_edu_toast_content
+ GestureType.OVERVIEW -> R.string.overview_edu_toast_content
+ GestureType.ALL_APPS -> R.string.all_apps_edu_toast_content
+ }
+ return resources.getString(resourceId)
+ }
+}
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/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 9f33113..871d046 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -645,6 +645,9 @@
public void showDismissibleKeyguard() {
trace("showDismissibleKeyguard");
checkPermission();
+ if (mFoldGracePeriodProvider.get().isEnabled()) {
+ mKeyguardInteractor.showDismissibleKeyguard();
+ }
mKeyguardViewMediator.showDismissibleKeyguard();
if (SceneContainerFlag.isEnabled() && mFoldGracePeriodProvider.get().isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 0b3d0f7..1f3df95 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2334,6 +2334,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.
@@ -2399,6 +2405,16 @@
*/
private void handleDismiss(IKeyguardDismissCallback callback, CharSequence message) {
if (mShowing) {
+ if (KeyguardWmStateRefactor.isEnabled()) {
+ Log.d(TAG, "Dismissing keyguard with keyguard_wm_refactor_enabled: "
+ + "cancelDoKeyguardLaterLocked");
+
+ // This won't get canceled in onKeyguardExitFinished() if the refactor is enabled,
+ // which can lead to the keyguard re-showing. Cancel here for now; this can be
+ // removed once we migrate the logic that posts doKeyguardLater in the first place.
+ cancelDoKeyguardLaterLocked();
+ }
+
if (callback != null) {
mDismissCallbackRegistry.addCallback(callback);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index edf17c1..81b0064 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -232,6 +232,9 @@
/** Receive an event for doze time tick */
val dozeTimeTick: Flow<Long>
+ /** Receive an event lockscreen being shown in a dismissible state */
+ val showDismissibleKeyguard: MutableStateFlow<Long>
+
/** Observable for DismissAction */
val dismissAction: StateFlow<DismissAction>
@@ -305,6 +308,8 @@
fun dozeTimeTick()
+ fun showDismissibleKeyguard()
+
fun setDismissAction(dismissAction: DismissAction)
suspend fun setKeyguardDone(keyguardDoneType: KeyguardDone)
@@ -439,6 +444,12 @@
_dozeTimeTick.value = systemClock.uptimeMillis()
}
+ override val showDismissibleKeyguard = MutableStateFlow<Long>(0L)
+
+ override fun showDismissibleKeyguard() {
+ showDismissibleKeyguard.value = systemClock.uptimeMillis()
+ }
+
private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
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/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 8f4110c..db5a63b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -74,6 +74,7 @@
listenForGoneToAodOrDozing()
listenForGoneToDreaming()
listenForGoneToLockscreenOrHub()
+ listenForGoneToOccluded()
listenForGoneToDreamingLockscreenHosted()
}
@@ -81,6 +82,27 @@
scope.launch("$TAG#showKeyguard") { startTransitionTo(KeyguardState.LOCKSCREEN) }
}
+ /**
+ * A special case supported on foldables, where folding the device may put the device on an
+ * unlocked lockscreen, but if an occluding app is already showing (like a active phone call),
+ * then go directly to OCCLUDED.
+ */
+ private fun listenForGoneToOccluded() {
+ scope.launch("$TAG#listenForGoneToOccluded") {
+ keyguardInteractor.showDismissibleKeyguard
+ .filterRelevantKeyguardState()
+ .sample(keyguardInteractor.isKeyguardOccluded, ::Pair)
+ .collect { (_, isKeyguardOccluded) ->
+ if (isKeyguardOccluded) {
+ startTransitionTo(
+ KeyguardState.OCCLUDED,
+ ownerReason = "Dismissible keyguard with occlusion"
+ )
+ }
+ }
+ }
+ }
+
// Primarily for when the user chooses to lock down the device
private fun listenForGoneToLockscreenOrHub() {
if (KeyguardWmStateRefactor.isEnabled) {
@@ -166,11 +188,12 @@
interpolator = Interpolators.LINEAR
duration =
when (toState) {
- KeyguardState.DREAMING -> TO_DREAMING_DURATION
KeyguardState.AOD -> TO_AOD_DURATION
KeyguardState.DOZING -> TO_DOZING_DURATION
+ KeyguardState.DREAMING -> TO_DREAMING_DURATION
KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+ KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
else -> DEFAULT_DURATION
}.inWholeMilliseconds
}
@@ -179,10 +202,11 @@
companion object {
private const val TAG = "FromGoneTransitionInteractor"
private val DEFAULT_DURATION = 500.milliseconds
- val TO_DREAMING_DURATION = 933.milliseconds
val TO_AOD_DURATION = 1300.milliseconds
val TO_DOZING_DURATION = 933.milliseconds
+ val TO_DREAMING_DURATION = 933.milliseconds
val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
+ val TO_OCCLUDED_DURATION = 100.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 51d92f0..5dc020f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -300,7 +300,9 @@
swipeToDismissInteractor.dismissFling
.filterNotNull()
.filterRelevantKeyguardState()
- .collect { _ -> startTransitionTo(KeyguardState.GONE) }
+ .collect { _ ->
+ startTransitionTo(KeyguardState.GONE, ownerReason = "dismissFling != null")
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 710b710a..aea57ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -157,6 +157,13 @@
}
}
+ /** Starts a transition to dismiss the keyguard from the OCCLUDED state. */
+ fun dismissFromOccluded() {
+ scope.launch {
+ startTransitionTo(KeyguardState.GONE, ownerReason = "Dismiss from occluded")
+ }
+ }
+
private fun listenForOccludedToGone() {
if (KeyguardWmStateRefactor.isEnabled) {
// We don't think OCCLUDED to GONE is possible. You should always have to go via a
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 69e10d9..0df989e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -1,18 +1,17 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
@@ -180,6 +179,9 @@
val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> =
repository.onCameraLaunchDetected.filter { it.type != CameraLaunchType.IGNORE }
+ /** Event for when an unlocked keyguard has been requested, such as on device fold */
+ val showDismissibleKeyguard: Flow<Long> = repository.showDismissibleKeyguard.asStateFlow()
+
/**
* Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
* that doze mode is not running and DREAMING is ok to commence.
@@ -490,6 +492,10 @@
CameraLaunchSourceModel(type = cameraLaunchSourceIntToType(source))
}
+ fun showDismissibleKeyguard() {
+ repository.showDismissibleKeyguard()
+ }
+
companion object {
private const val TAG = "KeyguardInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index afbe357..efdae62 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -75,6 +75,7 @@
private val fromAlternateBouncerTransitionInteractor:
dagger.Lazy<FromAlternateBouncerTransitionInteractor>,
private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>,
+ private val fromOccludedTransitionInteractor: dagger.Lazy<FromOccludedTransitionInteractor>,
private val sceneInteractor: SceneInteractor,
) {
private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>()
@@ -418,6 +419,7 @@
fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer()
AOD -> fromAodTransitionInteractor.get().dismissAod()
DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing()
+ KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded()
KeyguardState.GONE ->
Log.i(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
index 86e4115..906d586 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
@@ -19,14 +19,15 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.Utils.Companion.sample
-import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
/**
* Handles logic around the swipe to dismiss gesture, where the user swipes up on the dismissable
@@ -53,12 +54,14 @@
shadeRepository.currentFling
.sample(
transitionInteractor.startedKeyguardState,
- keyguardInteractor.isKeyguardDismissible
+ keyguardInteractor.isKeyguardDismissible,
+ keyguardInteractor.statusBarState,
)
- .filter { (flingInfo, startedState, keyguardDismissable) ->
+ .filter { (flingInfo, startedState, keyguardDismissable, statusBarState) ->
flingInfo != null &&
- !flingInfo.expand &&
- startedState == KeyguardState.LOCKSCREEN &&
+ !flingInfo.expand &&
+ statusBarState != StatusBarState.SHADE_LOCKED &&
+ startedState == KeyguardState.LOCKSCREEN &&
keyguardDismissable
}
.map { (flingInfo, _) -> flingInfo }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index e1b333d..25b2b7c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -93,6 +93,14 @@
KeyguardState.ALTERNATE_BOUNCER -> {
fromAlternateBouncerInteractor.surfaceBehindVisibility
}
+ KeyguardState.OCCLUDED -> {
+ // OCCLUDED -> GONE occurs when an app is on top of the keyguard, and then
+ // requests manual dismissal of the keyguard in the background. The app will
+ // remain visible on top of the stack throughout this transition, so we
+ // should not trigger the keyguard going away animation by returning
+ // surfaceBehindVisibility = true.
+ flowOf(false)
+ }
else -> flowOf(null)
}
}
@@ -253,6 +261,18 @@
) {
// Dreams dismiss keyguard and return to GONE if they can.
false
+ } else if (
+ startedWithPrev.newValue.from == KeyguardState.OCCLUDED &&
+ startedWithPrev.newValue.to == KeyguardState.GONE
+ ) {
+ // OCCLUDED -> GONE directly, without transiting a *_BOUNCER state, occurs
+ // when an app uses intent flags to launch over an insecure keyguard without
+ // dismissing it, and then manually requests keyguard dismissal while
+ // OCCLUDED. This transition is not user-visible; the device unlocks in the
+ // background and the app remains on top, while we're now GONE. In this case
+ // we should simply tell WM that the lockscreen is no longer visible, and
+ // *not* play the going away animation or related animations.
+ false
} else {
// Otherwise, use the visibility of the current state.
KeyguardState.lockscreenVisibleInState(currentState)
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/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index 162a0d2..15e6b1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -30,18 +30,23 @@
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launch
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.settingslib.Utils
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.doOnEnd
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -49,11 +54,20 @@
import kotlinx.coroutines.launch
/** This is only for a SINGLE Quick affordance */
-object KeyguardQuickAffordanceViewBinder {
+@SysUISingleton
+class KeyguardQuickAffordanceViewBinder
+@Inject
+constructor(
+ private val falsingManager: FalsingManager?,
+ private val vibratorHelper: VibratorHelper?,
+ private val logger: KeyguardQuickAffordancesLogger,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+) {
- private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
- private const val SCALE_SELECTED_BUTTON = 1.23f
- private const val DIM_ALPHA = 0.3f
+ private val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
+ private val SCALE_SELECTED_BUTTON = 1.23f
+ private val DIM_ALPHA = 0.3f
+ private val TAG = "KeyguardQuickAffordanceViewBinder"
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
@@ -73,30 +87,24 @@
view: LaunchableImageView,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
alpha: Flow<Float>,
- falsingManager: FalsingManager?,
- vibratorHelper: VibratorHelper?,
- logger: KeyguardQuickAffordancesLogger,
messageDisplayer: (Int) -> Unit,
): Binding {
val button = view as ImageView
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
val disposableHandle =
- view.repeatWhenAttached {
+ view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
+ launch("$TAG#viewModel") {
viewModel.collect { buttonModel ->
updateButton(
view = button,
viewModel = buttonModel,
- falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
- vibratorHelper = vibratorHelper,
- logger = logger,
)
}
}
- launch {
+ launch("$TAG#updateButtonAlpha") {
updateButtonAlpha(
view = button,
viewModel = viewModel,
@@ -104,7 +112,7 @@
)
}
- launch {
+ launch("$TAG#configurationBasedDimensions") {
configurationBasedDimensions.collect { dimensions ->
button.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
@@ -131,10 +139,7 @@
private fun updateButton(
view: ImageView,
viewModel: KeyguardQuickAffordanceViewModel,
- falsingManager: FalsingManager?,
messageDisplayer: (Int) -> Unit,
- vibratorHelper: VibratorHelper?,
- logger: KeyguardQuickAffordancesLogger,
) {
if (!viewModel.isVisible) {
view.isInvisible = true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 6faca1e..6031ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -50,7 +50,6 @@
import com.android.internal.policy.SystemBarUtils
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -79,7 +78,6 @@
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style
-import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -89,7 +87,6 @@
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants
import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -133,8 +130,6 @@
private val broadcastDispatcher: BroadcastDispatcher,
private val lockscreenSmartspaceController: LockscreenSmartspaceController,
private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
- private val falsingManager: FalsingManager,
- private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val keyguardRootViewModel: KeyguardRootViewModel,
private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
@@ -148,7 +143,7 @@
private val defaultShortcutsSection: DefaultShortcutsSection,
private val keyguardClockInteractor: KeyguardClockInteractor,
private val keyguardClockViewModel: KeyguardClockViewModel,
- private val quickAffordancesLogger: KeyguardQuickAffordancesLogger,
+ private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
) {
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -458,13 +453,10 @@
keyguardRootView.findViewById<LaunchableImageView?>(R.id.start_button)?.let { imageView ->
shortcutsBindings.add(
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
view = imageView,
viewModel = quickAffordancesCombinedViewModel.startButton,
alpha = flowOf(1f),
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
- logger = quickAffordancesLogger,
) { message ->
indicationController.showTransientIndication(message)
}
@@ -473,13 +465,10 @@
keyguardRootView.findViewById<LaunchableImageView?>(R.id.end_button)?.let { imageView ->
shortcutsBindings.add(
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
view = imageView,
viewModel = quickAffordancesCombinedViewModel.endButton,
alpha = flowOf(1f),
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
- logger = quickAffordancesLogger,
) { message ->
indicationController.showTransientIndication(message)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
index 2dc9301..bf6f2c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
@@ -42,12 +42,6 @@
): KeyguardBlueprint
@Binds
- @IntoSet
- abstract fun bindShortcutsBesideUdfpsLockscreenBlueprint(
- shortcutsBesideUdfpsLockscreenBlueprint: ShortcutsBesideUdfpsKeyguardBlueprint
- ): KeyguardBlueprint
-
- @Binds
@IntoMap
@ClassKey(KeyguardBlueprintInteractor::class)
abstract fun bindsKeyguardBlueprintInteractor(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
deleted file mode 100644
index b984a68..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
+++ /dev/null
@@ -1,94 +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.keyguard.ui.view.layout.blueprints
-
-import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialIndicatorSection
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
-import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AccessibilityActionsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AlignShortcutsToUdfpsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
-import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
-import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection
-import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
-import com.android.systemui.util.kotlin.getOrNull
-import java.util.Optional
-import javax.inject.Inject
-import javax.inject.Named
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-/** Vertically aligns the shortcuts with the udfps. */
-@ExperimentalCoroutinesApi
-@SysUISingleton
-class ShortcutsBesideUdfpsKeyguardBlueprint
-@Inject
-constructor(
- accessibilityActionsSection: AccessibilityActionsSection,
- alignShortcutsToUdfpsSection: AlignShortcutsToUdfpsSection,
- defaultIndicationAreaSection: DefaultIndicationAreaSection,
- defaultDeviceEntrySection: DefaultDeviceEntrySection,
- @Named(KeyguardSectionsModule.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION)
- defaultAmbientIndicationAreaSection: Optional<KeyguardSection>,
- defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
- defaultStatusViewSection: DefaultStatusViewSection,
- defaultStatusBarSection: DefaultStatusBarSection,
- defaultNotificationStackScrollLayoutSection: DefaultNotificationStackScrollLayoutSection,
- aodNotificationIconsSection: AodNotificationIconsSection,
- aodBurnInSection: AodBurnInSection,
- communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
- clockSection: ClockSection,
- smartspaceSection: SmartspaceSection,
- keyguardSliceViewSection: KeyguardSliceViewSection,
- udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
-) : KeyguardBlueprint {
- override val id: String = SHORTCUTS_BESIDE_UDFPS
-
- override val sections =
- listOfNotNull(
- accessibilityActionsSection,
- defaultIndicationAreaSection,
- alignShortcutsToUdfpsSection,
- defaultAmbientIndicationAreaSection.getOrNull(),
- defaultSettingsPopupMenuSection,
- defaultStatusViewSection,
- defaultStatusBarSection,
- defaultNotificationStackScrollLayoutSection,
- aodNotificationIconsSection,
- smartspaceSection,
- aodBurnInSection,
- communalTutorialIndicatorSection,
- clockSection,
- keyguardSliceViewSection,
- defaultDeviceEntrySection,
- udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
- )
-
- companion object {
- const val SHORTCUTS_BESIDE_UDFPS = "shortcuts-besides-udfps"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
deleted file mode 100644
index 1ba830b..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ /dev/null
@@ -1,114 +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.keyguard.ui.view.layout.sections
-
-import android.content.res.Resources
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.LEFT
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.RIGHT
-import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
-import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
-import javax.inject.Inject
-
-class AlignShortcutsToUdfpsSection
-@Inject
-constructor(
- @Main private val resources: Resources,
- private val keyguardQuickAffordancesCombinedViewModel:
- KeyguardQuickAffordancesCombinedViewModel,
- private val keyguardRootViewModel: KeyguardRootViewModel,
- private val falsingManager: FalsingManager,
- private val indicationController: KeyguardIndicationController,
- private val vibratorHelper: VibratorHelper,
- private val shortcutsLogger: KeyguardQuickAffordancesLogger,
-) : BaseShortcutSection() {
- override fun addViews(constraintLayout: ConstraintLayout) {
- if (KeyguardBottomAreaRefactor.isEnabled) {
- addLeftShortcut(constraintLayout)
- addRightShortcut(constraintLayout)
- }
- }
-
- override fun bindData(constraintLayout: ConstraintLayout) {
- if (KeyguardBottomAreaRefactor.isEnabled) {
- leftShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
- constraintLayout.requireViewById(R.id.start_button),
- keyguardQuickAffordancesCombinedViewModel.startButton,
- keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
- ) {
- indicationController.showTransientIndication(it)
- }
- rightShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
- constraintLayout.requireViewById(R.id.end_button),
- keyguardQuickAffordancesCombinedViewModel.endButton,
- keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
- ) {
- indicationController.showTransientIndication(it)
- }
- }
- }
-
- override fun applyConstraints(constraintSet: ConstraintSet) {
- val width = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width)
- val height = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
-
- val lockIconViewId =
- if (DeviceEntryUdfpsRefactor.isEnabled) {
- R.id.device_entry_icon_view
- } else {
- R.id.lock_icon_view
- }
-
- constraintSet.apply {
- constrainWidth(R.id.start_button, width)
- constrainHeight(R.id.start_button, height)
- connect(R.id.start_button, LEFT, PARENT_ID, LEFT)
- connect(R.id.start_button, RIGHT, lockIconViewId, LEFT)
- connect(R.id.start_button, TOP, lockIconViewId, TOP)
- connect(R.id.start_button, BOTTOM, lockIconViewId, BOTTOM)
-
- constrainWidth(R.id.end_button, width)
- constrainHeight(R.id.end_button, height)
- connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT)
- connect(R.id.end_button, LEFT, lockIconViewId, RIGHT)
- connect(R.id.end_button, TOP, lockIconViewId, TOP)
- connect(R.id.end_button, BOTTOM, lockIconViewId, BOTTOM)
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 64c46db..e558033 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -26,7 +26,6 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
@@ -35,10 +34,8 @@
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
import dagger.Lazy
import javax.inject.Inject
@@ -49,11 +46,9 @@
private val keyguardQuickAffordancesCombinedViewModel:
KeyguardQuickAffordancesCombinedViewModel,
private val keyguardRootViewModel: KeyguardRootViewModel,
- private val falsingManager: FalsingManager,
private val indicationController: KeyguardIndicationController,
- private val vibratorHelper: VibratorHelper,
private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
- private val shortcutsLogger: KeyguardQuickAffordancesLogger,
+ private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
) : BaseShortcutSection() {
// Amount to increase the bottom margin by to avoid colliding with inset
@@ -82,24 +77,18 @@
override fun bindData(constraintLayout: ConstraintLayout) {
if (KeyguardBottomAreaRefactor.isEnabled) {
leftShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
constraintLayout.requireViewById(R.id.start_button),
keyguardQuickAffordancesCombinedViewModel.startButton,
keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
) {
indicationController.showTransientIndication(it)
}
rightShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
constraintLayout.requireViewById(R.id.end_button),
keyguardQuickAffordancesCombinedViewModel.endButton,
keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
) {
indicationController.showTransientIndication(it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
index c590f07..992550c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
@@ -47,6 +48,15 @@
edge = Edge.create(from = ALTERNATE_BOUNCER, to = AOD),
)
+ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+ var startAlpha = 1f
+ return transitionAnimation.sharedFlow(
+ duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
+ onStart = { startAlpha = viewState.alpha() },
+ onStep = { MathUtils.lerp(startAlpha, 1f, it) },
+ )
+ }
+
val deviceEntryBackgroundViewAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
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/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
index 680f966..2426f97 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
@@ -19,6 +19,7 @@
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.FlowTracing.traceEmissionCount
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -29,19 +30,23 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardQuickAffordancesCombinedViewModel
@Inject
constructor(
+ @Application private val applicationScope: CoroutineScope,
private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
private val keyguardInteractor: KeyguardInteractor,
shadeInteractor: ShadeInteractor,
@@ -133,9 +138,14 @@
/** The source of truth of alpha for all of the quick affordances on lockscreen */
val transitionAlpha: Flow<Float> =
merge(
- fadeInAlpha,
- fadeOutAlpha,
- )
+ fadeInAlpha,
+ fadeOutAlpha,
+ )
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = 0f,
+ )
/**
* Whether quick affordances are "opaque enough" to be considered visible to and interactive by
@@ -199,38 +209,42 @@
private fun button(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceViewModel> {
- return previewMode.flatMapLatest { previewMode ->
- combine(
- if (previewMode.isInPreviewMode) {
- quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
- } else {
- quickAffordanceInteractor.quickAffordance(position = position)
- },
- keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
- areQuickAffordancesFullyOpaque,
- selectedPreviewSlotId,
- quickAffordanceInteractor.useLongPress(),
- ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
- val slotId = position.toSlotId()
- val isSelected = selectedPreviewSlotId == slotId
- model.toViewModel(
- animateReveal = !previewMode.isInPreviewMode && animateReveal,
- isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
- isSelected =
- previewMode.isInPreviewMode &&
- previewMode.shouldHighlightSelectedAffordance &&
- isSelected,
- isDimmed =
- previewMode.isInPreviewMode &&
- previewMode.shouldHighlightSelectedAffordance &&
- !isSelected,
- forceInactive = previewMode.isInPreviewMode,
- slotId = slotId,
- useLongPress = useLongPress,
- )
- }
- .distinctUntilChanged()
- }.traceEmissionCount({"QuickAfforcances#button${position.toSlotId()}"})
+ return previewMode
+ .flatMapLatest { previewMode ->
+ combine(
+ if (previewMode.isInPreviewMode) {
+ quickAffordanceInteractor.quickAffordanceAlwaysVisible(
+ position = position
+ )
+ } else {
+ quickAffordanceInteractor.quickAffordance(position = position)
+ },
+ keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
+ areQuickAffordancesFullyOpaque,
+ selectedPreviewSlotId,
+ quickAffordanceInteractor.useLongPress(),
+ ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
+ val slotId = position.toSlotId()
+ val isSelected = selectedPreviewSlotId == slotId
+ model.toViewModel(
+ animateReveal = !previewMode.isInPreviewMode && animateReveal,
+ isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
+ isSelected =
+ previewMode.isInPreviewMode &&
+ previewMode.shouldHighlightSelectedAffordance &&
+ isSelected,
+ isDimmed =
+ previewMode.isInPreviewMode &&
+ previewMode.shouldHighlightSelectedAffordance &&
+ !isSelected,
+ forceInactive = previewMode.isInPreviewMode,
+ slotId = slotId,
+ useLongPress = useLongPress,
+ )
+ }
+ .distinctUntilChanged()
+ }
+ .traceEmissionCount({ "QuickAfforcances#button${position.toSlotId()}" })
}
private fun KeyguardQuickAffordanceModel.toViewModel(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 38a2b1b..050ef6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -83,6 +83,7 @@
private val communalInteractor: CommunalInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
+ private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel,
private val alternateBouncerToGoneTransitionViewModel:
AlternateBouncerToGoneTransitionViewModel,
private val alternateBouncerToLockscreenTransitionViewModel:
@@ -239,6 +240,7 @@
merge(
alphaOnShadeExpansion,
keyguardInteractor.dismissAlpha,
+ alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState),
alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
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/navigationbar/NavigationBarModule.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java
index e2ba761..a8b979e0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java
@@ -16,11 +16,16 @@
package com.android.systemui.navigationbar;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
+
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
+import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
@@ -28,6 +33,7 @@
import com.android.systemui.navigationbar.views.NavigationBarView;
import com.android.systemui.res.R;
+import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
@@ -73,4 +79,15 @@
static WindowManager provideWindowManager(@DisplayId Context context) {
return context.getSystemService(WindowManager.class);
}
+
+ /** A ViewCaptureAwareWindowManager specific to the display's context. */
+ @Provides
+ @NavigationBarScope
+ @DisplayId
+ static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager(
+ @DisplayId WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) {
+ return new ViewCaptureAwareWindowManager(windowManager,
+ /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture),
+ /* isViewCaptureEnabled= */ enableViewCaptureTracing());
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index 7b248eb..e895d83 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -102,6 +102,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEvent;
@@ -196,6 +197,7 @@
private final Context mContext;
private final Bundle mSavedState;
private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
private final AccessibilityManager mAccessibilityManager;
private final DeviceProvisionedController mDeviceProvisionedController;
private final StatusBarStateController mStatusBarStateController;
@@ -556,6 +558,7 @@
@Nullable Bundle savedState,
@DisplayId Context context,
@DisplayId WindowManager windowManager,
+ @DisplayId ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
Lazy<AssistManager> assistManagerLazy,
AccessibilityManager accessibilityManager,
DeviceProvisionedController deviceProvisionedController,
@@ -601,6 +604,7 @@
mContext = context;
mSavedState = savedState;
mWindowManager = windowManager;
+ mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
mAccessibilityManager = accessibilityManager;
mDeviceProvisionedController = deviceProvisionedController;
mStatusBarStateController = statusBarStateController;
@@ -721,7 +725,7 @@
if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mView);
- mWindowManager.addView(mFrame,
+ mViewCaptureAwareWindowManager.addView(mFrame,
getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
.getRotation()));
mDisplayId = mContext.getDisplayId();
@@ -764,7 +768,7 @@
mCommandQueue.removeCallback(this);
Trace.beginSection("NavigationBar#removeViewImmediate");
try {
- mWindowManager.removeViewImmediate(mView.getRootView());
+ mViewCaptureAwareWindowManager.removeViewImmediate(mView.getRootView());
} finally {
Trace.endSection();
}
@@ -866,7 +870,7 @@
if (mOrientationHandle != null) {
resetSecondaryHandle();
getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener);
- mWindowManager.removeView(mOrientationHandle);
+ mViewCaptureAwareWindowManager.removeView(mOrientationHandle);
mOrientationHandle.getViewTreeObserver().removeOnGlobalLayoutListener(
mOrientationHandleGlobalLayoutListener);
}
@@ -937,7 +941,7 @@
mOrientationParams.setTitle("SecondaryHomeHandle" + mContext.getDisplayId());
mOrientationParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION
| WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
- mWindowManager.addView(mOrientationHandle, mOrientationParams);
+ mViewCaptureAwareWindowManager.addView(mOrientationHandle, mOrientationParams);
mOrientationHandle.setVisibility(View.GONE);
logNavbarOrientation("initSecondaryHomeHandleForRotation");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java
index 1b34c33..89be17b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java
@@ -19,6 +19,7 @@
import android.database.ContentObserver;
import android.os.Handler;
+import com.android.systemui.Flags;
import com.android.systemui.statusbar.policy.Listenable;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.settings.SystemSettings;
@@ -76,10 +77,20 @@
mListening = listening;
if (listening) {
mObservedValue = getValueFromProvider();
- mSettingsProxy.registerContentObserverForUserSync(
- mSettingsProxy.getUriFor(mSettingName), false, this, mUserId);
+ if (Flags.qsRegisterSettingObserverOnBgThread()) {
+ mSettingsProxy.registerContentObserverForUserAsync(
+ mSettingsProxy.getUriFor(mSettingName), this, mUserId, () ->
+ mObservedValue = getValueFromProvider());
+ } else {
+ mSettingsProxy.registerContentObserverForUserSync(
+ mSettingsProxy.getUriFor(mSettingName), false, this, mUserId);
+ }
} else {
- mSettingsProxy.unregisterContentObserverSync(this);
+ if (Flags.qsRegisterSettingObserverOnBgThread()) {
+ mSettingsProxy.unregisterContentObserverAsync(this);
+ } else {
+ mSettingsProxy.unregisterContentObserverSync(this);
+ }
mObservedValue = mDefaultValue;
}
}
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/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 98a61df..863a899 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -24,6 +24,7 @@
import android.net.Uri
import android.os.Handler
import android.os.UserHandle
+import android.util.Log
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.dagger.qualifiers.LongRunning
@@ -71,6 +72,7 @@
override fun provideRecordingServiceStrings(): RecordingServiceStrings = IrsStrings(resources)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(getTag(), "handling action: ${intent?.action}")
when (intent?.action) {
ACTION_START -> {
bgExecutor.execute {
@@ -95,7 +97,7 @@
bgExecutor.execute {
mNotificationManager.cancelAsUser(
null,
- mNotificationId,
+ intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
UserHandle(mUserContextTracker.userContext.userId)
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/OWNERS b/packages/SystemUI/src/com/android/systemui/scene/OWNERS
index 033ff14..2ffcad4 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/scene/OWNERS
@@ -1,2 +1,13 @@
+set noparent
+
+# Bug component: 1215786
+
justinweir@google.com
nijamkin@google.com
+
+# SysUI Dr No's.
+# Don't send reviews here.
+cinek@google.com
+dsandler@android.com
+juliacr@google.com
+pixel@google.com
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/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 1170354..700253ba 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -63,7 +63,9 @@
protected static final int NOTIF_BASE_ID = 4273;
private static final String TAG = "RecordingService";
private static final String CHANNEL_ID = "screen_record";
- private static final String GROUP_KEY = "screen_record_saved";
+ @VisibleForTesting static final String GROUP_KEY_SAVED = "screen_record_saved";
+ private static final String GROUP_KEY_ERROR_STARTING = "screen_record_error_starting";
+ @VisibleForTesting static final String GROUP_KEY_ERROR_SAVING = "screen_record_error_saving";
private static final String EXTRA_RESULT_CODE = "extra_resultCode";
protected static final String EXTRA_PATH = "extra_path";
private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
@@ -78,6 +80,7 @@
"com.android.systemui.screenrecord.STOP_FROM_NOTIF";
protected static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+ protected static final String EXTRA_NOTIFICATION_ID = "notification_id";
private final RecordingController mController;
protected final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -181,7 +184,7 @@
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
} else {
updateState(false);
- createErrorStartingNotification();
+ createErrorStartingNotification(currentUser);
stopForeground(STOP_FOREGROUND_DETACH);
stopSelf();
return Service.START_NOT_STICKY;
@@ -276,8 +279,8 @@
* errors.
*/
@VisibleForTesting
- protected void createErrorStartingNotification() {
- createErrorNotification(strings().getStartError());
+ protected void createErrorStartingNotification(UserHandle currentUser) {
+ createErrorNotification(currentUser, strings().getStartError(), GROUP_KEY_ERROR_STARTING);
}
/**
@@ -285,17 +288,22 @@
* errors.
*/
@VisibleForTesting
- protected void createErrorSavingNotification() {
- createErrorNotification(strings().getSaveError());
+ protected void createErrorSavingNotification(UserHandle currentUser) {
+ createErrorNotification(currentUser, strings().getSaveError(), GROUP_KEY_ERROR_SAVING);
}
- private void createErrorNotification(String notificationContentTitle) {
+ private void createErrorNotification(
+ UserHandle currentUser, String notificationContentTitle, String groupKey) {
+ // Make sure error notifications get their own group.
+ postGroupSummaryNotification(currentUser, notificationContentTitle, groupKey);
+
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle());
Notification.Builder builder = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
.setContentTitle(notificationContentTitle)
+ .setGroup(groupKey)
.addExtras(extras);
startForeground(mNotificationId, builder.build());
}
@@ -350,7 +358,7 @@
.setContentText(
strings().getBackgroundProcessingLabel())
.setSmallIcon(R.drawable.ic_screenrecord)
- .setGroup(GROUP_KEY)
+ .setGroup(GROUP_KEY_SAVED)
.addExtras(extras);
return builder.build();
}
@@ -387,7 +395,7 @@
PendingIntent.FLAG_IMMUTABLE))
.addAction(shareAction)
.setAutoCancel(true)
- .setGroup(GROUP_KEY)
+ .setGroup(GROUP_KEY_SAVED)
.addExtras(extras);
// Add thumbnail if available
@@ -402,21 +410,28 @@
}
/**
- * Adds a group notification so that save notifications from multiple recordings are
- * grouped together, and the foreground service recording notification is not
+ * Adds a group summary notification for save notifications so that save notifications from
+ * multiple recordings are grouped together, and the foreground service recording notification
+ * is not.
*/
- private void postGroupNotification(UserHandle currentUser) {
+ private void postGroupSummaryNotificationForSaves(UserHandle currentUser) {
+ postGroupSummaryNotification(currentUser, strings().getSaveTitle(), GROUP_KEY_SAVED);
+ }
+
+ /** Posts a group summary notification for the given group. */
+ private void postGroupSummaryNotification(
+ UserHandle currentUser, String notificationContentTitle, String groupKey) {
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
strings().getTitle());
Notification groupNotif = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
- .setContentTitle(strings().getSaveTitle())
- .setGroup(GROUP_KEY)
+ .setContentTitle(notificationContentTitle)
+ .setGroup(groupKey)
.setGroupSummary(true)
.setExtras(extras)
.build();
- mNotificationManager.notifyAsUser(getTag(), NOTIF_BASE_ID, groupNotif, currentUser);
+ mNotificationManager.notifyAsUser(getTag(), mNotificationId, groupNotif, currentUser);
}
private void stopService() {
@@ -427,6 +442,7 @@
if (userId == USER_ID_NOT_SPECIFIED) {
userId = mUserContextTracker.getUserContext().getUserId();
}
+ UserHandle currentUser = new UserHandle(userId);
Log.d(getTag(), "notifying for user " + userId);
setTapsVisible(mOriginalShowTaps);
try {
@@ -444,7 +460,7 @@
Log.e(getTag(), "stopRecording called, but there was an error when ending"
+ "recording");
exception.printStackTrace();
- createErrorSavingNotification();
+ createErrorSavingNotification(currentUser);
} catch (Throwable throwable) {
if (getRecorder() != null) {
// Something unexpected happen, SystemUI will crash but let's delete
@@ -468,7 +484,7 @@
Log.d(getTag(), "saving recording");
Notification notification = createSaveNotification(
getRecorder() != null ? getRecorder().save() : null);
- postGroupNotification(currentUser);
+ postGroupSummaryNotificationForSaves(currentUser);
mNotificationManager.notifyAsUser(null, mNotificationId, notification,
currentUser);
} catch (IOException | IllegalStateException e) {
@@ -527,7 +543,8 @@
private Intent getShareIntent(Context context, Uri path) {
return new Intent(context, this.getClass()).setAction(ACTION_SHARE)
- .putExtra(EXTRA_PATH, path);
+ .putExtra(EXTRA_PATH, path)
+ .putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
}
@Override
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/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 0a092a0..16aef65 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -2252,8 +2252,11 @@
// panel, mQs will not need to be null cause it will be tied to the same lifecycle.
if (fragment == mQs) {
// Clear it to remove bindings to mQs from the provider.
- mNotificationStackScrollLayoutController.setQsHeaderBoundsProvider(null);
- mNotificationStackScrollLayoutController.setQsHeader(null);
+ if (QSComposeFragment.isEnabled()) {
+ mNotificationStackScrollLayoutController.setQsHeaderBoundsProvider(null);
+ } else {
+ mNotificationStackScrollLayoutController.setQsHeader(null);
+ }
mQs = null;
}
}
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/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index 9e221d3..f48e31e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import javax.inject.Inject
@@ -46,6 +47,10 @@
sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
repository: ShadeRepository,
) : BaseShadeInteractor {
+ init {
+ SceneContainerFlag.assertInLegacyMode()
+ }
+
/**
* The amount [0-1] that the shade has been opened. Uses stateIn to avoid redundant calculations
* in downstream flows.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 9617b54..6a21531 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -22,8 +22,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -43,8 +44,12 @@
constructor(
@Application scope: CoroutineScope,
sceneInteractor: SceneInteractor,
- sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
+ shadeRepository: ShadeRepository,
) : BaseShadeInteractor {
+ init {
+ SceneContainerFlag.assertInNewMode()
+ }
+
override val shadeExpansion: StateFlow<Float> =
sceneBasedExpansion(sceneInteractor, SceneFamilies.NotifShade)
.traceAsCounter("panel_expansion") { (it * 100f).toInt() }
@@ -55,7 +60,7 @@
override val qsExpansion: StateFlow<Float> =
combine(
- sharedNotificationContainerInteractor.isSplitShadeEnabled,
+ shadeRepository.isShadeLayoutWide,
shadeExpansion,
sceneBasedQsExpansion,
) { isSplitShadeEnabled, shadeExpansion, qsExpansion ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 1027bc9..9b382e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -89,6 +89,7 @@
.filter { it.callType == CallType.Ongoing }
.minByOrNull { it.whenTime }
}
+ .distinctUntilChanged()
.flowOn(backgroundDispatcher)
/** Are any notifications being actively presented in the notification stack? */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 2537aff..5d37476 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -23,7 +23,9 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.LargeScreenHeaderHelper
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.policy.SplitShadeStateController
import dagger.Lazy
import javax.inject.Inject
@@ -43,7 +45,8 @@
constructor(
configurationRepository: ConfigurationRepository,
private val context: Context,
- private val splitShadeStateController: SplitShadeStateController,
+ private val splitShadeStateController: Lazy<SplitShadeStateController>,
+ private val shadeInteractor: Lazy<ShadeInteractor>,
keyguardInteractor: KeyguardInteractor,
deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
@@ -56,16 +59,33 @@
/** An internal modification was made to notifications */
val notificationStackChanged = _notificationStackChanged.debounce(20L)
+ private val configurationChangeEvents =
+ configurationRepository.onAnyConfigurationChange.onStart { emit(Unit) }
+
+ /* Warning: Even though the value it emits only contains the split shade status, this flow must
+ * emit a value whenever the configuration *or* the split shade status changes. Adding a
+ * distinctUntilChanged() to this would cause configurationBasedDimensions to miss configuration
+ * updates that affect other resources, like margins or the large screen header flag.
+ */
+ private val dimensionsUpdateEventsWithShouldUseSplitShade: Flow<Boolean> =
+ if (SceneContainerFlag.isEnabled) {
+ combine(configurationChangeEvents, shadeInteractor.get().isShadeLayoutWide) {
+ _,
+ isShadeLayoutWide ->
+ isShadeLayoutWide
+ }
+ } else {
+ configurationChangeEvents.map {
+ splitShadeStateController.get().shouldUseSplitNotificationShade(context.resources)
+ }
+ }
+
val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
- configurationRepository.onAnyConfigurationChange
- .onStart { emit(Unit) }
- .map { _ ->
+ dimensionsUpdateEventsWithShouldUseSplitShade
+ .map { shouldUseSplitShade ->
with(context.resources) {
ConfigurationBasedDimensions(
- useSplitShade =
- splitShadeStateController.shouldUseSplitNotificationShade(
- context.resources
- ),
+ useSplitShade = shouldUseSplitShade,
useLargeScreenHeader =
getBoolean(R.bool.config_use_large_screen_shade_header),
marginHorizontal =
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/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 0541550..aa1911e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -99,19 +99,20 @@
disposables +=
view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- // Only temporarily needed, until flexi notifs go live
- viewModel.shadeCollapseFadeIn.collect { fadeIn ->
- if (fadeIn) {
- android.animation.ValueAnimator.ofFloat(0f, 1f).apply {
- duration = 250
- addUpdateListener { animation ->
- controller.setMaxAlphaForKeyguard(
- animation.animatedFraction,
- "SharedNotificationContainerVB (collapseFadeIn)"
- )
+ if (!SceneContainerFlag.isEnabled) {
+ launch {
+ viewModel.shadeCollapseFadeIn.collect { fadeIn ->
+ if (fadeIn) {
+ android.animation.ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 250
+ addUpdateListener { animation ->
+ controller.setMaxAlphaForKeyguard(
+ animation.animatedFraction,
+ "SharedNotificationContainerVB (collapseFadeIn)"
+ )
+ }
+ start()
}
- start()
}
}
}
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 b6d58d6..ada3c52 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2353,7 +2353,11 @@
&& !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
&& mStatusBarKeyguardViewManager.isSecure()) {
Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
- mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+ if (SceneContainerFlag.isEnabled()) {
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+ } else {
+ mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+ }
}
}
}
@@ -2985,7 +2989,7 @@
@Override
public void onFalse() {
// Hides quick settings, bouncer, and quick-quick settings.
- mStatusBarKeyguardViewManager.reset(true);
+ mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
}
};
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 2d775b7..5486abb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
* Shows the notification keyguard or the bouncer depending on
* {@link #needsFullscreenBouncer()}.
*/
- protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
boolean isDozing = mDozing;
if (Flags.simPinRaceConditionOnRestart()) {
KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,8 +734,11 @@
mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
}
}
- } else {
- Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
+ } else if (!isFalsingReset) {
+ // Falsing resets can cause this to flicker, so don't reset in this case
+ Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+
}
} else {
mCentralSurfaces.showKeyguard();
@@ -957,6 +960,10 @@
@Override
public void reset(boolean hideBouncerWhenShowing) {
+ reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
+ }
+
+ public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
final boolean isOccluded = mKeyguardStateController.isOccluded();
// Hide quick settings.
@@ -965,10 +972,14 @@
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);
+ showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
}
if (hideBouncerWhenShowing) {
hideAlternateBouncer(true);
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/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt
new file mode 100644
index 0000000..f7f2631
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/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.touchpad.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.res.R
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED
+
+@Composable
+fun ActionKeyTutorialScreen(
+ 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/touchpad/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt
new file mode 100644
index 0000000..2b7f674
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/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.touchpad.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.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED
+
+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/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 9ac2cba..dfe9c0d 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,18 @@
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.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 +42,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 +73,11 @@
)
}
TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
- GestureTutorialContent(gestureState, onDoneButtonClicked, screenConfig)
+ ActionTutorialContent(
+ gestureState.toTutorialActionState(),
+ onDoneButtonClicked,
+ screenConfig
+ )
}
}
@@ -135,169 +108,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/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index ad8ab30..48e6397 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.touchpad.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/util/settings/GlobalSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
index 816f55d..7fcabe4 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
@@ -42,28 +42,31 @@
mBgDispatcher = bgDispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Settings.Global.getUriFor(name);
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mBgDispatcher;
}
@Override
- public String getString(String name) {
+ public String getString(@NonNull String name) {
return Settings.Global.getString(mContentResolver, name);
}
@Override
- public boolean putString(String name, String value) {
+ public boolean putString(@NonNull String name, String value) {
return Settings.Global.putString(mContentResolver, name, value);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
index f1da27f..c296481 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
@@ -16,12 +16,11 @@
package com.android.systemui.util.settings;
+import android.annotation.NonNull;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.Settings;
-import androidx.annotation.NonNull;
-
import com.android.systemui.util.kotlin.SettingsSingleThreadBackground;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -43,46 +42,50 @@
mBgDispatcher = bgDispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
+ @NonNull
@Override
public CurrentUserIdProvider getCurrentUserProvider() {
return mCurrentUserProvider;
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Settings.Secure.getUriFor(name);
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mBgDispatcher;
}
@Override
- public String getStringForUser(String name, int userHandle) {
+ public String getStringForUser(@NonNull String name, int userHandle) {
return Settings.Secure.getStringForUser(mContentResolver, name,
getRealUserHandle(userHandle));
}
@Override
- public boolean putString(String name, String value, boolean overrideableByRestore) {
+ public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) {
return Settings.Secure.putString(mContentResolver, name, value, overrideableByRestore);
}
@Override
- public boolean putStringForUser(String name, String value, int userHandle) {
+ public boolean putStringForUser(@NonNull String name, String value, int userHandle) {
return Settings.Secure.putStringForUser(mContentResolver, name, value,
getRealUserHandle(userHandle));
}
@Override
- public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
- int userHandle, boolean overrideableByRestore) {
+ public boolean putStringForUser(@NonNull String name, String value, String tag,
+ boolean makeDefault, int userHandle, boolean overrideableByRestore) {
return Settings.Secure.putStringForUser(
mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle),
overrideableByRestore);
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
index 0ee997e..82f41a7 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -346,7 +346,7 @@
* @param value to associate with the name
* @return true if the value was set, false on database errors
*/
- fun putString(name: String, value: String): Boolean
+ fun putString(name: String, value: String?): Boolean
/**
* Store a name/value pair into the database.
@@ -377,7 +377,7 @@
* @return true if the value was set, false on database errors.
* @see .resetToDefaults
*/
- fun putString(name: String, value: String, tag: String, makeDefault: Boolean): Boolean
+ fun putString(name: String, value: String?, tag: String?, makeDefault: Boolean): Boolean
/**
* Convenience function for retrieving a single secure settings value as an integer. Note that
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
index 1e80357..e670b2c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
@@ -16,12 +16,11 @@
package com.android.systemui.util.settings;
+import android.annotation.NonNull;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.Settings;
-import androidx.annotation.NonNull;
-
import com.android.systemui.util.kotlin.SettingsSingleThreadBackground;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -42,46 +41,50 @@
mBgCoroutineDispatcher = bgDispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
+ @NonNull
@Override
public CurrentUserIdProvider getCurrentUserProvider() {
return mCurrentUserProvider;
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Settings.System.getUriFor(name);
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mBgCoroutineDispatcher;
}
@Override
- public String getStringForUser(String name, int userHandle) {
+ public String getStringForUser(@NonNull String name, int userHandle) {
return Settings.System.getStringForUser(mContentResolver, name,
getRealUserHandle(userHandle));
}
@Override
- public boolean putString(String name, String value, boolean overrideableByRestore) {
+ public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) {
return Settings.System.putString(mContentResolver, name, value, overrideableByRestore);
}
@Override
- public boolean putStringForUser(String name, String value, int userHandle) {
+ public boolean putStringForUser(@NonNull String name, String value, int userHandle) {
return Settings.System.putStringForUser(mContentResolver, name, value,
getRealUserHandle(userHandle));
}
@Override
- public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
- int userHandle, boolean overrideableByRestore) {
+ public boolean putStringForUser(@NonNull String name, String value, String tag,
+ boolean makeDefault, int userHandle, boolean overrideableByRestore) {
throw new UnsupportedOperationException(
"This method only exists publicly for Settings.Secure and Settings.Global");
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 9ae8f03..8e3b813 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -368,19 +368,19 @@
* @param value to associate with the name
* @return true if the value was set, false on database errors
*/
- fun putString(name: String, value: String, overrideableByRestore: Boolean): Boolean
+ fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean
- override fun putString(name: String, value: String): Boolean {
+ override fun putString(name: String, value: String?): Boolean {
return putStringForUser(name, value, userId)
}
/** Similar implementation to [putString] for the specified [userHandle]. */
- fun putStringForUser(name: String, value: String, userHandle: Int): Boolean
+ fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean
/** Similar implementation to [putString] for the specified [userHandle]. */
fun putStringForUser(
name: String,
- value: String,
+ value: String?,
tag: String?,
makeDefault: Boolean,
@UserIdInt userHandle: Int,
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/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java
new file mode 100644
index 0000000..2ac5d10
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.accessibility.hearingaid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.testing.TestableLooper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Tests for {@link HearingDevicesPresetsController}. */
+@RunWith(AndroidJUnit4.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesPresetsControllerTest extends SysuiTestCase {
+
+ private static final int TEST_PRESET_INDEX = 1;
+ private static final String TEST_PRESET_NAME = "test_preset";
+ private static final int TEST_HAP_GROUP_ID = 1;
+ private static final int TEST_REASON = 1024;
+
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private LocalBluetoothProfileManager mProfileManager;
+ @Mock
+ private HapClientProfile mHapClientProfile;
+ @Mock
+ private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock
+ private CachedBluetoothDevice mSubCachedBluetoothDevice;
+ @Mock
+ private BluetoothDevice mBluetoothDevice;
+ @Mock
+ private BluetoothDevice mSubBluetoothDevice;
+
+ @Mock
+ private HearingDevicesPresetsController.PresetCallback mCallback;
+
+ private HearingDevicesPresetsController mController;
+
+ @Before
+ public void setUp() {
+ when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
+ when(mHapClientProfile.isProfileReady()).thenReturn(true);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mCachedBluetoothDevice.getSubDevice()).thenReturn(mSubCachedBluetoothDevice);
+ when(mSubCachedBluetoothDevice.getDevice()).thenReturn(mSubBluetoothDevice);
+
+ mController = new HearingDevicesPresetsController(mProfileManager, mCallback);
+ }
+
+ @Test
+ public void onServiceConnected_callExpectedCallback() {
+ mController.onServiceConnected();
+
+ verify(mHapClientProfile).registerCallback(any(Executor.class),
+ any(BluetoothHapClient.Callback.class));
+ verify(mCallback).onPresetInfoUpdated(anyList(), anyInt());
+ }
+
+ @Test
+ public void getAllPresetInfo_setInvalidHearingDevice_getEmpty() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(emptyList());
+ mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice);
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+
+ assertThat(mController.getAllPresetInfo()).isEmpty();
+ }
+
+ @Test
+ public void getAllPresetInfo_containsNotAvailablePresetInfo_getEmpty() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(false);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+
+ assertThat(mController.getAllPresetInfo()).isEmpty();
+ }
+
+ @Test
+ public void getAllPresetInfo_containsOnePresetInfo_getOnePresetInfo() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+
+ assertThat(mController.getAllPresetInfo()).contains(hapPresetInfo);
+ }
+
+ @Test
+ public void getActivePresetIndex_getExpectedIndex() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+ TEST_PRESET_INDEX);
+
+ assertThat(mController.getActivePresetIndex()).isEqualTo(TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void onPresetSelected_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+ when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+ TEST_PRESET_INDEX);
+
+ mController.onPresetSelected(mBluetoothDevice, TEST_PRESET_INDEX, TEST_REASON);
+
+ verify(mCallback).onPresetInfoUpdated(eq(List.of(hapPresetInfo)), eq(TEST_PRESET_INDEX));
+ }
+
+ @Test
+ public void onPresetInfoChanged_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+ when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+ TEST_PRESET_INDEX);
+
+ mController.onPresetInfoChanged(mBluetoothDevice, List.of(hapPresetInfo), TEST_REASON);
+
+ verify(mCallback).onPresetInfoUpdated(List.of(hapPresetInfo), TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void onPresetSelectionFailed_callOnPresetCommandFailed() {
+ setValidHearingDeviceSupportHap();
+
+ mController.onPresetSelectionFailed(mBluetoothDevice, TEST_REASON);
+
+ verify(mCallback).onPresetCommandFailed(TEST_REASON);
+ }
+
+ @Test
+ public void onSetPresetNameFailed_callOnPresetCommandFailed() {
+ setValidHearingDeviceSupportHap();
+
+ mController.onSetPresetNameFailed(mBluetoothDevice, TEST_REASON);
+
+ verify(mCallback).onPresetCommandFailed(TEST_REASON);
+ }
+
+ @Test
+ public void onPresetSelectionForGroupFailed_callSelectPresetIndividual() {
+ setValidHearingDeviceSupportHap();
+ mController.selectPreset(TEST_PRESET_INDEX);
+ Mockito.reset(mHapClientProfile);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+ mController.onPresetSelectionForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON);
+
+
+ verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+ verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void onSetPresetNameForGroupFailed_callOnPresetCommandFailed() {
+ setValidHearingDeviceSupportHap();
+
+ mController.onSetPresetNameForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON);
+
+ verify(mCallback).onPresetCommandFailed(TEST_REASON);
+ }
+
+ @Test
+ public void registerHapCallback_callHapRegisterCallback() {
+ mController.registerHapCallback();
+
+ verify(mHapClientProfile).registerCallback(any(Executor.class),
+ any(BluetoothHapClient.Callback.class));
+ }
+
+ @Test
+ public void unregisterHapCallback_callHapUnregisterCallback() {
+ mController.unregisterHapCallback();
+
+ verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class));
+ }
+
+ @Test
+ public void selectPreset_supportSynchronized_validGroupId_callSelectPresetForGroup() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+ mController.selectPreset(TEST_PRESET_INDEX);
+
+ verify(mHapClientProfile).selectPresetForGroup(TEST_HAP_GROUP_ID, TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void selectPreset_supportSynchronized_invalidGroupId_callSelectPresetIndividual() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+
+ mController.selectPreset(TEST_PRESET_INDEX);
+
+ verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+ verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void selectPreset_notSupportSynchronized_validGroupId_callSelectPresetIndividual() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(false);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+ mController.selectPreset(TEST_PRESET_INDEX);
+
+ verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+ verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+ }
+
+ private BluetoothHapPresetInfo getHapPresetInfo(boolean available) {
+ BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class);
+ when(info.getName()).thenReturn(TEST_PRESET_NAME);
+ when(info.getIndex()).thenReturn(TEST_PRESET_INDEX);
+ when(info.isAvailable()).thenReturn(available);
+ return info;
+ }
+
+ private void setValidHearingDeviceSupportHap() {
+ LocalBluetoothProfile hapClientProfile = mock(HapClientProfile.class);
+ List<LocalBluetoothProfile> profiles = List.of(hapClientProfile);
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(profiles);
+
+ mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice);
+ }
+}
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/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/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/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 77977f3..24bea2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -195,9 +195,7 @@
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
val featureFlags =
- FakeFeatureFlags().apply {
- set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
- }
+ FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) }
val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
keyguardInteractor = withDeps.keyguardInteractor
@@ -289,6 +287,7 @@
underTest =
KeyguardQuickAffordancesCombinedViewModel(
+ applicationScope = testScope.backgroundScope,
quickAffordanceInteractor =
KeyguardQuickAffordanceInteractor(
keyguardInteractor = keyguardInteractor,
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/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
index a52ab0c..04d140c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
@@ -78,6 +78,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
@@ -214,6 +215,8 @@
@Mock
private WindowManager mWindowManager;
@Mock
+ private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
+ @Mock
private TelecomManager mTelecomManager;
@Mock
private InputMethodManager mInputMethodManager;
@@ -619,6 +622,7 @@
null,
context,
mWindowManager,
+ mViewCaptureAwareWindowManager,
() -> mAssistManager,
mock(AccessibilityManager.class),
deviceProvisionedController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
index 90e0dd8..0c2b59f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
@@ -17,25 +17,34 @@
package com.android.systemui.qs
import android.os.Handler
+import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.fail
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
private typealias Callback = (Int, Boolean) -> Unit
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-class UserSettingObserverTest : SysuiTestCase() {
+class UserSettingObserverTest(flags: FlagsParameterization) : SysuiTestCase() {
companion object {
private const val TEST_SETTING = "setting"
@@ -43,8 +52,23 @@
private const val OTHER_USER = 1
private const val DEFAULT_VALUE = 1
private val FAIL_CALLBACK: Callback = { _, _ -> fail("Callback should not be called") }
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD
+ )
+ }
}
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+
private lateinit var testableLooper: TestableLooper
private lateinit var setting: UserSettingObserver
private lateinit var secureSettings: SecureSettings
@@ -54,7 +78,7 @@
@Before
fun setUp() {
testableLooper = TestableLooper.get(this)
- secureSettings = FakeSettings()
+ secureSettings = kosmos.fakeSettings
setting =
object :
@@ -76,92 +100,107 @@
@After
fun tearDown() {
- setting.isListening = false
+ setListening(false)
}
@Test
- fun testNotListeningByDefault() {
- callback = FAIL_CALLBACK
+ fun testNotListeningByDefault() =
+ testScope.runTest {
+ callback = FAIL_CALLBACK
- assertThat(setting.isListening).isFalse()
- secureSettings.putIntForUser(TEST_SETTING, 2, USER)
- testableLooper.processAllMessages()
- }
+ assertThat(setting.isListening).isFalse()
+ secureSettings.putIntForUser(TEST_SETTING, 2, USER)
+ testableLooper.processAllMessages()
+ }
@Test
- fun testChangedWhenListeningCallsCallback() {
- var changed = false
- callback = { _, _ -> changed = true }
+ fun testChangedWhenListeningCallsCallback() =
+ testScope.runTest {
+ var changed = false
+ callback = { _, _ -> changed = true }
- setting.isListening = true
- secureSettings.putIntForUser(TEST_SETTING, 2, USER)
- testableLooper.processAllMessages()
+ setListening(true)
+ secureSettings.putIntForUser(TEST_SETTING, 2, USER)
+ testableLooper.processAllMessages()
- assertThat(changed).isTrue()
- }
+ assertThat(changed).isTrue()
+ }
@Test
- fun testListensToCorrectSetting() {
- callback = FAIL_CALLBACK
+ fun testListensToCorrectSetting() =
+ testScope.runTest {
+ callback = FAIL_CALLBACK
- setting.isListening = true
- secureSettings.putIntForUser("other", 2, USER)
- testableLooper.processAllMessages()
- }
+ setListening(true)
+ secureSettings.putIntForUser("other", 2, USER)
+ testableLooper.processAllMessages()
+ }
@Test
- fun testGetCorrectValue() {
- secureSettings.putIntForUser(TEST_SETTING, 2, USER)
- assertThat(setting.value).isEqualTo(2)
+ fun testGetCorrectValue() =
+ testScope.runTest {
+ secureSettings.putIntForUser(TEST_SETTING, 2, USER)
+ assertThat(setting.value).isEqualTo(2)
- secureSettings.putIntForUser(TEST_SETTING, 4, USER)
- assertThat(setting.value).isEqualTo(4)
- }
+ secureSettings.putIntForUser(TEST_SETTING, 4, USER)
+ assertThat(setting.value).isEqualTo(4)
+ }
@Test
- fun testSetValue() {
- setting.value = 5
- assertThat(secureSettings.getIntForUser(TEST_SETTING, USER)).isEqualTo(5)
- }
+ fun testSetValue() =
+ testScope.runTest {
+ setting.value = 5
+ assertThat(secureSettings.getIntForUser(TEST_SETTING, USER)).isEqualTo(5)
+ }
@Test
- fun testChangeUser() {
- setting.isListening = true
- setting.setUserId(OTHER_USER)
+ fun testChangeUser() =
+ testScope.runTest {
+ setListening(true)
+ setting.setUserId(OTHER_USER)
- setting.isListening = true
- assertThat(setting.currentUser).isEqualTo(OTHER_USER)
- }
+ setListening(true)
+ assertThat(setting.currentUser).isEqualTo(OTHER_USER)
+ }
@Test
- fun testDoesntListenInOtherUsers() {
- callback = FAIL_CALLBACK
- setting.isListening = true
+ fun testDoesntListenInOtherUsers() =
+ testScope.runTest {
+ callback = FAIL_CALLBACK
+ setListening(true)
- secureSettings.putIntForUser(TEST_SETTING, 3, OTHER_USER)
- testableLooper.processAllMessages()
- }
+ secureSettings.putIntForUser(TEST_SETTING, 3, OTHER_USER)
+ testableLooper.processAllMessages()
+ }
@Test
- fun testListensToCorrectUserAfterChange() {
- var changed = false
- callback = { _, _ -> changed = true }
+ fun testListensToCorrectUserAfterChange() =
+ testScope.runTest {
+ var changed = false
+ callback = { _, _ -> changed = true }
- setting.isListening = true
- setting.setUserId(OTHER_USER)
- secureSettings.putIntForUser(TEST_SETTING, 2, OTHER_USER)
- testableLooper.processAllMessages()
+ setListening(true)
+ setting.setUserId(OTHER_USER)
+ testScope.runCurrent()
+ secureSettings.putIntForUser(TEST_SETTING, 2, OTHER_USER)
+ testableLooper.processAllMessages()
- assertThat(changed).isTrue()
- }
+ assertThat(changed).isTrue()
+ }
@Test
- fun testDefaultValue() {
- // Check default value before listening
- assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
+ fun testDefaultValue() =
+ testScope.runTest {
+ // Check default value before listening
+ assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
- // Check default value if setting is not set
- setting.isListening = true
- assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
+ // Check default value if setting is not set
+ setListening(true)
+ assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
+ }
+
+ fun setListening(listening: Boolean) {
+ setting.isListening = listening
+ testScope.runCurrent()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 79c206c..3f550ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -16,8 +16,13 @@
package com.android.systemui.screenrecord;
+import static com.android.systemui.screenrecord.RecordingService.GROUP_KEY_ERROR_SAVING;
+import static com.android.systemui.screenrecord.RecordingService.GROUP_KEY_SAVED;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +30,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -73,8 +79,6 @@
@Mock
private ScreenMediaRecorder mScreenMediaRecorder;
@Mock
- private Notification mNotification;
- @Mock
private Executor mExecutor;
@Mock
private Handler mHandler;
@@ -124,10 +128,6 @@
// Mock notifications
doNothing().when(mRecordingService).createRecordingNotification();
- doReturn(mNotification).when(mRecordingService).createProcessingNotification();
- doReturn(mNotification).when(mRecordingService).createSaveNotification(any());
- doNothing().when(mRecordingService).createErrorStartingNotification();
- doNothing().when(mRecordingService).createErrorSavingNotification();
doNothing().when(mRecordingService).showErrorToast(anyInt());
doNothing().when(mRecordingService).stopForeground(anyInt());
@@ -228,6 +228,33 @@
}
@Test
+ public void testOnSystemRequestedStop_whenRecordingInProgress_showsNotifications() {
+ doReturn(true).when(mController).isRecording();
+
+ mRecordingService.onStopped();
+
+ // Processing notification
+ ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
+ verify(mNotificationManager).notifyAsUser(any(), anyInt(), notifCaptor.capture(), any());
+ assertEquals(GROUP_KEY_SAVED, notifCaptor.getValue().getGroup());
+
+ reset(mNotificationManager);
+ verify(mExecutor).execute(mRunnableCaptor.capture());
+ mRunnableCaptor.getValue().run();
+
+ verify(mNotificationManager, times(2))
+ .notifyAsUser(any(), anyInt(), notifCaptor.capture(), any());
+ // Saved notification
+ Notification saveNotification = notifCaptor.getAllValues().get(0);
+ assertFalse(saveNotification.isGroupSummary());
+ assertEquals(GROUP_KEY_SAVED, saveNotification.getGroup());
+ // Group summary notification
+ Notification groupSummaryNotification = notifCaptor.getAllValues().get(1);
+ assertTrue(groupSummaryNotification.isGroupSummary());
+ assertEquals(GROUP_KEY_SAVED, groupSummaryNotification.getGroup());
+ }
+
+ @Test
public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_showsErrorNotification()
throws IOException {
doReturn(true).when(mController).isRecording();
@@ -235,7 +262,11 @@
mRecordingService.onStopped();
- verify(mRecordingService).createErrorSavingNotification();
+ verify(mRecordingService).createErrorSavingNotification(any());
+ ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
+ verify(mNotificationManager).notifyAsUser(any(), anyInt(), notifCaptor.capture(), any());
+ assertTrue(notifCaptor.getValue().isGroupSummary());
+ assertEquals(GROUP_KEY_ERROR_SAVING, notifCaptor.getValue().getGroup());
}
@Test
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/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 2803035..8125ef5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -192,6 +192,7 @@
import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.unfold.SysUIUnfoldComponent;
@@ -428,6 +429,9 @@
mock(DeviceEntryUdfpsInteractor.class);
when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false));
+ final SplitShadeStateController splitShadeStateController =
+ new ResourcesSplitShadeStateController();
+
mShadeInteractor = new ShadeInteractorImpl(
mTestScope.getBackgroundScope(),
mKosmos.getDeviceProvisioningInteractor(),
@@ -445,7 +449,8 @@
new SharedNotificationContainerInteractor(
new FakeConfigurationRepository(),
mContext,
- new ResourcesSplitShadeStateController(),
+ () -> splitShadeStateController,
+ () -> mShadeInteractor,
mKeyguardInteractor,
deviceEntryUdfpsInteractor,
() -> mLargeScreenHeaderHelper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index e57382d..505f799 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -225,7 +225,8 @@
new SharedNotificationContainerInteractor(
configurationRepository,
mContext,
- splitShadeStateController,
+ () -> splitShadeStateController,
+ () -> mShadeInteractor,
keyguardInteractor,
deviceEntryUdfpsInteractor,
() -> mLargeScreenHeaderHelper),
@@ -266,6 +267,7 @@
when(mPanelView.getParent()).thenReturn(mPanelViewParent);
when(mQs.getHeader()).thenReturn(mQsHeader);
+ when(mQSFragment.getHeader()).thenReturn(mQsHeader);
doAnswer(invocation -> {
mLockscreenShadeTransitionCallback = invocation.getArgument(0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index e7db469..2e9d6e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
@@ -24,34 +24,41 @@
import static android.view.MotionEvent.BUTTON_SECONDARY;
import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY;
-import static com.android.systemui.Flags.FLAG_QS_UI_REFACTOR;
import static com.android.systemui.Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.flags.QSComposeFragment;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import java.util.List;
@@ -65,7 +72,7 @@
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return progressionOf(FLAG_QS_UI_REFACTOR, FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT);
+ return progressionOf(FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT);
}
public QuickSettingsControllerImplTest(FlagsParameterization flags) {
@@ -244,6 +251,61 @@
}
@Test
+ @DisableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQsFragmentAttached_qsComposeFragmentDisabled_setHeaderInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController)
+ .setQsHeader((ViewGroup) mQSFragment.getHeader());
+ verify(mNotificationStackScrollLayoutController, never()).setQsHeaderBoundsProvider(any());
+ }
+
+ @Test
+ @EnableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQsFragmentAttached_qsComposeFragmentEnabled_setQsHeaderBoundsProviderInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController, never())
+ .setQsHeader((ViewGroup) mQSFragment.getHeader());
+ ArgumentCaptor<QSHeaderBoundsProvider> argumentCaptor =
+ ArgumentCaptor.forClass(QSHeaderBoundsProvider.class);
+
+ verify(mNotificationStackScrollLayoutController)
+ .setQsHeaderBoundsProvider(argumentCaptor.capture());
+
+ argumentCaptor.getValue().getLeftProvider().invoke();
+ argumentCaptor.getValue().getHeightProvider().invoke();
+ argumentCaptor.getValue().getBoundsOnScreenProvider().invoke(new Rect());
+ InOrder inOrderVerifier = inOrder(mQSFragment);
+
+ inOrderVerifier.verify(mQSFragment).getHeaderLeft();
+ inOrderVerifier.verify(mQSFragment).getHeaderHeight();
+ inOrderVerifier.verify(mQSFragment).getHeaderBoundsOnScreen(new Rect());
+ }
+
+ @Test
+ @DisableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQSFragmentDetached_qsComposeFragmentFlagDisabled_setViewToNullInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ mFragmentListener.onFragmentViewDestroyed(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController).setQsHeader(null);
+ verify(mNotificationStackScrollLayoutController, never()).setQsHeaderBoundsProvider(null);
+ }
+
+ @Test
+ @EnableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQSFragmentDetached_qsComposeFragmentFlagEnabled_setBoundsProviderToNullInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ mFragmentListener.onFragmentViewDestroyed(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController, never()).setQsHeader(null);
+ verify(mNotificationStackScrollLayoutController).setQsHeaderBoundsProvider(null);
+ }
+
+ @Test
public void onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS() {
setIsFullWidth(false);
mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 3f28164..491919a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel
+import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
@@ -78,7 +79,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
-@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME)
+@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME, LockscreenOtpRedaction.FLAG_NAME)
class NotificationRowContentBinderImplTest : SysuiTestCase() {
private lateinit var notificationInflater: NotificationRowContentBinderImpl
private lateinit var builder: Notification.Builder
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index af5e60e..9b61105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
public void testShowBouncerOrKeyguard_needsFullScreen() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
verify(mCentralSurfaces).hideKeyguard();
verify(mPrimaryBouncerInteractor).show(true);
}
@@ -1084,7 +1084,7 @@
.thenReturn(KeyguardState.LOCKSCREEN);
reset(mCentralSurfaces);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
verify(mPrimaryBouncerInteractor).show(true);
verify(mCentralSurfaces).showKeyguard();
}
@@ -1092,11 +1092,26 @@
@Test
@DisableSceneContainer
public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
+ boolean isFalsingReset = false;
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
verify(mCentralSurfaces, never()).hideKeyguard();
+ verify(mPrimaryBouncerInteractor).show(true);
+ }
+
+ @Test
+ @DisableSceneContainer
+ public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
+ boolean isFalsingReset = true;
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.SimPin);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+ verify(mCentralSurfaces, never()).hideKeyguard();
+
+ // Do not refresh the full screen bouncer if the call is from falsing
verify(mPrimaryBouncerInteractor, never()).show(true);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
index b0acd03..2e6d0fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
@@ -385,7 +385,7 @@
private class FakeSettingsProxy(val testDispatcher: CoroutineDispatcher) : SettingsProxy {
private val mContentResolver = mock(ContentResolver::class.java)
- private val settingToValueMap: MutableMap<String, String> = mutableMapOf()
+ private val settingToValueMap: MutableMap<String, String?> = mutableMapOf()
override fun getContentResolver() = mContentResolver
@@ -399,15 +399,15 @@
return settingToValueMap[name] ?: ""
}
- override fun putString(name: String, value: String): Boolean {
+ override fun putString(name: String, value: String?): Boolean {
settingToValueMap[name] = value
return true
}
override fun putString(
name: String,
- value: String,
- tag: String,
+ value: String?,
+ tag: String?,
makeDefault: Boolean
): Boolean {
settingToValueMap[name] = value
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
index eaeece9..00b8cd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -561,7 +561,7 @@
) : UserSettingsProxy {
private val mContentResolver = mock(ContentResolver::class.java)
- private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String>> =
+ private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String?>> =
mutableMapOf()
override fun getContentResolver() = mContentResolver
@@ -577,7 +577,7 @@
override fun putString(
name: String,
- value: String,
+ value: String?,
overrideableByRestore: Boolean
): Boolean {
userIdToSettingsValueMap[DEFAULT_USER_ID]?.put(name, value)
@@ -586,22 +586,22 @@
override fun putString(
name: String,
- value: String,
- tag: String,
+ value: String?,
+ tag: String?,
makeDefault: Boolean
): Boolean {
putStringForUser(name, value, DEFAULT_USER_ID)
return true
}
- override fun putStringForUser(name: String, value: String, userHandle: Int): Boolean {
+ override fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean {
userIdToSettingsValueMap[userHandle] = mutableMapOf(Pair(name, value))
return true
}
override fun putStringForUser(
name: String,
- value: String,
+ value: String?,
tag: String?,
makeDefault: Boolean,
userHandle: Int,
diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
index 185deea..a61233a 100644
--- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
@@ -16,10 +16,12 @@
package android.content
+import com.android.systemui.SysuiTestableContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
import com.android.systemui.util.mockito.mock
-var Kosmos.applicationContext: Context by
+var Kosmos.testableContext: SysuiTestableContext by
Kosmos.Fixture { testCase.context.apply { ensureTestableResources() } }
+var Kosmos.applicationContext: Context by Kosmos.Fixture { testableContext }
val Kosmos.mockedContext: Context by Kosmos.Fixture { mock<Context>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index a1c2f79..4571c19 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -74,6 +74,8 @@
private val _dozeTimeTick = MutableStateFlow<Long>(0L)
override val dozeTimeTick = _dozeTimeTick
+ override val showDismissibleKeyguard = MutableStateFlow<Long>(0L)
+
private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
@@ -206,6 +208,10 @@
_dozeTimeTick.value = millis
}
+ override fun showDismissibleKeyguard() {
+ showDismissibleKeyguard.value = showDismissibleKeyguard.value + 1
+ }
+
override fun setLastDozeTapToWakePosition(position: Point) {
_lastDozeTapToWakePosition.value = position
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index c5da10e..b68d6a0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -33,6 +33,7 @@
fromAodTransitionInteractor = { fromAodTransitionInteractor },
fromAlternateBouncerTransitionInteractor = { fromAlternateBouncerTransitionInteractor },
fromDozingTransitionInteractor = { fromDozingTransitionInteractor },
+ fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor },
sceneInteractor = sceneInteractor
)
}
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/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 82860fc..b9443bc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -39,6 +39,7 @@
communalInteractor = communalInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
notificationsKeyguardInteractor = notificationsKeyguardInteractor,
+ alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel,
alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
alternateBouncerToLockscreenTransitionViewModel =
alternateBouncerToLockscreenTransitionViewModel,
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt
similarity index 64%
copy from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt
index 460de8c..73b1859 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt
@@ -14,18 +14,11 @@
* limitations under the License.
*/
-package android.telephony.satellite.stub;
+package com.android.systemui.qs.tiles.impl.airplane
-/**
- * {@hide}
- */
-parcelable ProvisionSubscriberId {
- /** provision subscriberId */
- String subscriberId;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.connectivity.ConnectivityModule
- /** carrier id */
- int mCarrierId;
-
- /** apn */
- String mNiddApn;
-}
+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/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index ea02d0c..6d488d2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -18,10 +18,13 @@
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.SysuiTestableContext
+import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.data.repository.ShadeRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@@ -86,6 +89,11 @@
delegate.assertFlagValid()
delegate.setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer)
}
+
+ fun setSplitShade(splitShade: Boolean) {
+ delegate.assertFlagValid()
+ delegate.setSplitShade(splitShade)
+ }
}
/** Sets up shade state for tests for a specific value of the scene container flag. */
@@ -117,11 +125,16 @@
fun setQsFullscreen(qsFullscreen: Boolean)
fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean)
+
+ fun setSplitShade(splitShade: Boolean)
}
/** Sets up shade state for tests when the scene container flag is disabled. */
-class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: FakeShadeRepository) :
- ShadeTestUtilDelegate {
+class ShadeTestUtilLegacyImpl(
+ val testScope: TestScope,
+ val shadeRepository: FakeShadeRepository,
+ val context: SysuiTestableContext
+) : ShadeTestUtilDelegate {
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
shadeRepository.setLegacyShadeExpansion(shadeExpansion)
shadeRepository.setQsExpansion(qsExpansion)
@@ -168,11 +181,22 @@
override fun setLegacyExpandedOrAwaitingInputTransfer(expanded: Boolean) {
shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(expanded)
}
+
+ override fun setSplitShade(splitShade: Boolean) {
+ context
+ .getOrCreateTestableResources()
+ .addOverride(R.bool.config_use_split_notification_shade, splitShade)
+ testScope.runCurrent()
+ }
}
/** Sets up shade state for tests when the scene container flag is enabled. */
-class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: SceneInteractor) :
- ShadeTestUtilDelegate {
+class ShadeTestUtilSceneImpl(
+ val testScope: TestScope,
+ val sceneInteractor: SceneInteractor,
+ val shadeRepository: ShadeRepository,
+ val context: SysuiTestableContext,
+) : ShadeTestUtilDelegate {
val isUserInputOngoing = MutableStateFlow(true)
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
@@ -263,6 +287,14 @@
testScope.runCurrent()
}
+ override fun setSplitShade(splitShade: Boolean) {
+ context
+ .getOrCreateTestableResources()
+ .addOverride(R.bool.config_use_split_notification_shade, splitShade)
+ shadeRepository.setShadeLayoutWide(splitShade)
+ testScope.runCurrent()
+ }
+
override fun assertFlagValid() {
Assert.assertTrue(SceneContainerFlag.isEnabled)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
index 9eeb345..a1551e0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
+import android.content.testableContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -26,9 +27,14 @@
Kosmos.Fixture {
ShadeTestUtil(
if (SceneContainerFlag.isEnabled) {
- ShadeTestUtilSceneImpl(testScope, sceneInteractor)
+ ShadeTestUtilSceneImpl(
+ testScope,
+ sceneInteractor,
+ fakeShadeRepository,
+ testableContext
+ )
} else {
- ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository)
+ ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository, testableContext)
}
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index bfd6614..54208b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -44,7 +44,7 @@
ShadeInteractorSceneContainerImpl(
scope = applicationCoroutineScope,
sceneInteractor = sceneInteractor,
- sharedNotificationContainerInteractor = sharedNotificationContainerInteractor,
+ shadeRepository = shadeRepository,
)
}
val Kosmos.shadeInteractorLegacyImpl by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
index 8909d75..3234e66 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
@@ -21,6 +21,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.largeScreenHeaderHelper
import com.android.systemui.statusbar.policy.splitShadeStateController
@@ -29,7 +30,8 @@
SharedNotificationContainerInteractor(
configurationRepository = configurationRepository,
context = applicationContext,
- splitShadeStateController = splitShadeStateController,
+ splitShadeStateController = { splitShadeStateController },
+ shadeInteractor = { shadeInteractor },
keyguardInteractor = keyguardInteractor,
deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
deleted file mode 100644
index d117466..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.settings;
-
-import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
-
-import android.annotation.UserIdInt;
-import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.UserHandle;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import kotlinx.coroutines.CoroutineDispatcher;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class FakeSettings implements SecureSettings, SystemSettings {
- private final Map<SettingsKey, String> mValues = new HashMap<>();
- private final Map<SettingsKey, List<ContentObserver>> mContentObservers =
- new HashMap<>();
- private final Map<String, List<ContentObserver>> mContentObserversAllUsers = new HashMap<>();
- private final CoroutineDispatcher mDispatcher;
-
- public static final Uri CONTENT_URI = Uri.parse("content://settings/fake");
- @UserIdInt
- private int mUserId = UserHandle.USER_CURRENT;
-
- private final CurrentUserIdProvider mCurrentUserProvider;
-
- /**
- * @deprecated Please use FakeSettings(testDispatcher) to provide the same dispatcher used
- * by main test scope.
- */
- @Deprecated
- public FakeSettings() {
- mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
- mCurrentUserProvider = () -> mUserId;
- }
-
- public FakeSettings(CoroutineDispatcher dispatcher) {
- mDispatcher = dispatcher;
- mCurrentUserProvider = () -> mUserId;
- }
-
- public FakeSettings(CoroutineDispatcher dispatcher, CurrentUserIdProvider currentUserProvider) {
- mDispatcher = dispatcher;
- mCurrentUserProvider = currentUserProvider;
- }
-
- @VisibleForTesting
- FakeSettings(String initialKey, String initialValue) {
- this();
- putString(initialKey, initialValue);
- }
-
- @VisibleForTesting
- FakeSettings(Map<String, String> initialValues) {
- this();
- for (Map.Entry<String, String> kv : initialValues.entrySet()) {
- putString(kv.getKey(), kv.getValue());
- }
- }
-
- @Override
- @NonNull
- public ContentResolver getContentResolver() {
- throw new UnsupportedOperationException(
- "FakeSettings.getContentResolver is not implemented");
- }
-
- @NonNull
- @Override
- public CurrentUserIdProvider getCurrentUserProvider() {
- return mCurrentUserProvider;
- }
-
- @NonNull
- @Override
- public CoroutineDispatcher getBackgroundDispatcher() {
- return mDispatcher;
- }
-
- @Override
- public void registerContentObserverForUserSync(@NonNull Uri uri, boolean notifyDescendants,
- @NonNull ContentObserver settingsObserver, int userHandle) {
- List<ContentObserver> observers;
- if (userHandle == UserHandle.USER_ALL) {
- mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>());
- observers = mContentObserversAllUsers.get(uri.toString());
- } else {
- SettingsKey key = new SettingsKey(userHandle, uri.toString());
- mContentObservers.putIfAbsent(key, new ArrayList<>());
- observers = mContentObservers.get(key);
- }
- observers.add(settingsObserver);
- }
-
- @Override
- public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) {
- for (List<ContentObserver> observers : mContentObservers.values()) {
- observers.remove(settingsObserver);
- }
- for (List<ContentObserver> observers : mContentObserversAllUsers.values()) {
- observers.remove(settingsObserver);
- }
- }
-
- @NonNull
- @Override
- public Uri getUriFor(@NonNull String name) {
- return Uri.withAppendedPath(CONTENT_URI, name);
- }
-
- public void setUserId(@UserIdInt int userId) {
- mUserId = userId;
- }
-
- @Override
- public int getUserId() {
- return mUserId;
- }
-
- @Override
- public String getString(@NonNull String name) {
- return getStringForUser(name, getUserId());
- }
-
- @Override
- public String getStringForUser(@NonNull String name, int userHandle) {
- return mValues.get(new SettingsKey(userHandle, getUriFor(name).toString()));
- }
-
- @Override
- public boolean putString(@NonNull String name, @NonNull String value,
- boolean overrideableByRestore) {
- return putStringForUser(name, value, null, false, getUserId(), overrideableByRestore);
- }
-
- @Override
- public boolean putString(@NonNull String name, @NonNull String value) {
- return putString(name, value, false);
- }
-
- @Override
- public boolean putStringForUser(@NonNull String name, @NonNull String value, int userHandle) {
- return putStringForUser(name, value, null, false, userHandle, false);
- }
-
- @Override
- public boolean putStringForUser(@NonNull String name, @NonNull String value, String tag,
- boolean makeDefault, int userHandle, boolean overrideableByRestore) {
- SettingsKey key = new SettingsKey(userHandle, getUriFor(name).toString());
- mValues.put(key, value);
-
- Uri uri = getUriFor(name);
- for (ContentObserver observer : mContentObservers.getOrDefault(key, new ArrayList<>())) {
- observer.dispatchChange(false, List.of(uri), 0, userHandle);
- }
- for (ContentObserver observer :
- mContentObserversAllUsers.getOrDefault(uri.toString(), new ArrayList<>())) {
- observer.dispatchChange(false, List.of(uri), 0, userHandle);
- }
- return true;
- }
-
- @Override
- public boolean putString(@NonNull String name, @NonNull String value, @NonNull String tag,
- boolean makeDefault) {
- return putString(name, value);
- }
-
- private static class SettingsKey extends Pair<Integer, String> {
- SettingsKey(Integer first, String second) {
- super(first, second);
- }
- }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt
new file mode 100644
index 0000000..e5d113b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.settings
+
+import android.annotation.UserIdInt
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.UserHandle
+import android.util.Pair
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.util.settings.SettingsProxy.CurrentUserIdProvider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+
+class FakeSettings : SecureSettings, SystemSettings, UserSettingsProxy {
+ private val values = mutableMapOf<SettingsKey, String?>()
+ private val contentObservers = mutableMapOf<SettingsKey, MutableList<ContentObserver>>()
+ private val contentObserversAllUsers = mutableMapOf<String, MutableList<ContentObserver>>()
+
+ override val backgroundDispatcher: CoroutineDispatcher
+
+ @UserIdInt override var userId = UserHandle.USER_CURRENT
+ override val currentUserProvider: CurrentUserIdProvider
+
+ @Deprecated(
+ """Please use FakeSettings(testDispatcher) to provide the same dispatcher used
+ by main test scope."""
+ )
+ constructor() {
+ backgroundDispatcher = StandardTestDispatcher(scheduler = null, name = null)
+ currentUserProvider = CurrentUserIdProvider { userId }
+ }
+
+ constructor(dispatcher: CoroutineDispatcher) {
+ backgroundDispatcher = dispatcher
+ currentUserProvider = CurrentUserIdProvider { userId }
+ }
+
+ constructor(dispatcher: CoroutineDispatcher, currentUserProvider: CurrentUserIdProvider) {
+ backgroundDispatcher = dispatcher
+ this.currentUserProvider = currentUserProvider
+ }
+
+ @VisibleForTesting
+ internal constructor(initialKey: String, initialValue: String) : this() {
+ putString(initialKey, initialValue)
+ }
+
+ @VisibleForTesting
+ internal constructor(initialValues: Map<String, String>) : this() {
+ for ((key, value) in initialValues) {
+ putString(key, value)
+ }
+ }
+
+ override fun getContentResolver(): ContentResolver {
+ throw UnsupportedOperationException("FakeSettings.getContentResolver is not implemented")
+ }
+
+ override fun registerContentObserverForUserSync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) {
+ if (userHandle == UserHandle.USER_ALL) {
+ contentObserversAllUsers
+ .getOrPut(uri.toString()) { mutableListOf() }
+ .add(settingsObserver)
+ } else {
+ val key = SettingsKey(userHandle, uri.toString())
+ contentObservers.getOrPut(key) { mutableListOf() }.add(settingsObserver)
+ }
+ }
+
+ override fun unregisterContentObserverSync(settingsObserver: ContentObserver) {
+ contentObservers.values.onEach { it.remove(settingsObserver) }
+ contentObserversAllUsers.values.onEach { it.remove(settingsObserver) }
+ }
+
+ override suspend fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) =
+ suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserver(uri, settingsObserver)
+ }
+
+ override fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver): Job =
+ advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverAsync(uri, settingsObserver)
+ }
+
+ override suspend fun registerContentObserver(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserver(
+ uri,
+ notifyForDescendants,
+ settingsObserver
+ )
+ }
+
+ override fun registerContentObserverAsync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverAsync(
+ uri,
+ notifyForDescendants,
+ settingsObserver
+ )
+ }
+
+ override suspend fun registerContentObserverForUser(
+ name: String,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUser(name, settingsObserver, userHandle)
+ }
+
+ override fun registerContentObserverForUserAsync(
+ name: String,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ name,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun unregisterContentObserverAsync(settingsObserver: ContentObserver): Job =
+ advanceDispatcher {
+ super<UserSettingsProxy>.unregisterContentObserverAsync(settingsObserver)
+ }
+
+ override suspend fun registerContentObserverForUser(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUser(uri, settingsObserver, userHandle)
+ }
+
+ override fun registerContentObserverForUserAsync(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ uri,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun registerContentObserverForUserAsync(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ userHandle: Int,
+ registered: Runnable
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ uri,
+ settingsObserver,
+ userHandle,
+ registered
+ )
+ }
+
+ override suspend fun registerContentObserverForUser(
+ name: String,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUser(
+ name,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun registerContentObserverForUserAsync(
+ name: String,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ name,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun registerContentObserverForUserAsync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ uri,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun getUriFor(name: String): Uri {
+ return Uri.withAppendedPath(CONTENT_URI, name)
+ }
+
+ override fun getString(name: String): String? {
+ return getStringForUser(name, userId)
+ }
+
+ override fun getStringForUser(name: String, userHandle: Int): String? {
+ return values[SettingsKey(userHandle, getUriFor(name).toString())]
+ }
+
+ override fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean {
+ return putStringForUser(name, value, null, false, userId, overrideableByRestore)
+ }
+
+ override fun putString(name: String, value: String?): Boolean {
+ return putString(name, value, false)
+ }
+
+ override fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean {
+ return putStringForUser(name, value, null, false, userHandle, false)
+ }
+
+ override fun putStringForUser(
+ name: String,
+ value: String?,
+ tag: String?,
+ makeDefault: Boolean,
+ userHandle: Int,
+ overrideableByRestore: Boolean
+ ): Boolean {
+ val key = SettingsKey(userHandle, getUriFor(name).toString())
+ values[key] = value
+ val uri = getUriFor(name)
+ contentObservers[key]?.onEach { it.dispatchChange(false, listOf(uri), 0, userHandle) }
+ contentObserversAllUsers[uri.toString()]?.onEach {
+ it.dispatchChange(false, listOf(uri), 0, userHandle)
+ }
+ return true
+ }
+
+ override fun putString(
+ name: String,
+ value: String?,
+ tag: String?,
+ makeDefault: Boolean
+ ): Boolean {
+ return putString(name, value)
+ }
+
+ /** Runs current jobs on dispatcher after calling the method. */
+ private fun <T> advanceDispatcher(f: () -> T): T {
+ val result = f()
+ testDispatcherRunCurrent()
+ return result
+ }
+
+ private suspend fun <T> suspendAdvanceDispatcher(f: suspend () -> T): T {
+ val result = f()
+ testDispatcherRunCurrent()
+ return result
+ }
+
+ private fun testDispatcherRunCurrent() {
+ val testDispatcher = backgroundDispatcher as? TestDispatcher
+ testDispatcher?.scheduler?.runCurrent()
+ }
+
+ private data class SettingsKey(val first: Int, val second: String) :
+ Pair<Int, String>(first, second)
+
+ companion object {
+ val CONTENT_URI = Uri.parse("content://settings/fake")
+ }
+}
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/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index 68f185e..cc9b70e 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -281,6 +281,12 @@
com.android.server.LocalServices
+com.android.internal.graphics.cam.Cam
+com.android.internal.graphics.cam.CamUtils
+com.android.internal.graphics.cam.Frame
+com.android.internal.graphics.cam.HctSolver
+com.android.internal.graphics.ColorUtils
+
com.android.internal.util.BitUtils
com.android.internal.util.BitwiseInputStream
com.android.internal.util.BitwiseOutputStream
diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
index 56da231..2ce5c2b 100644
--- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
+++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
@@ -78,6 +78,9 @@
private final AccessibilityManagerService mAms;
private final Handler mHandler;
+ /** Thread to wait for virtual mouse creation to complete */
+ private final Thread mCreateVirtualMouseThread;
+
VirtualDeviceManager.VirtualDevice mVirtualDevice = null;
private VirtualMouse mVirtualMouse = null;
@@ -154,34 +157,47 @@
mHandler = new Handler(looper, this);
// Create the virtual mouse on a separate thread since virtual device creation
// should happen on an auxiliary thread, and not from the handler's thread.
- // This is because virtual device creation is a blocking operation and can cause a
- // deadlock if it is called from the handler's thread.
- new Thread(() -> {
+ // This is because the handler thread is the same as the main thread,
+ // and the main thread will be blocked waiting for the virtual device to be created.
+ mCreateVirtualMouseThread = new Thread(() -> {
mVirtualMouse = createVirtualMouse(displayId);
- }).start();
+ });
+ mCreateVirtualMouseThread.start();
+ }
+ /**
+ * Wait for {@code mVirtualMouse} to be created.
+ * This will ensure that {@code mVirtualMouse} is always created before
+ * trying to send mouse events.
+ **/
+ private void waitForVirtualMouseCreation() {
+ try {
+ // Block the current thread until the virtual mouse creation thread completes.
+ mCreateVirtualMouseThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
}
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private void sendVirtualMouseRelativeEvent(float x, float y) {
- if (mVirtualMouse != null) {
- mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder()
- .setRelativeX(x)
- .setRelativeY(y)
- .build()
- );
- }
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder()
+ .setRelativeX(x)
+ .setRelativeY(y)
+ .build()
+ );
}
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private void sendVirtualMouseButtonEvent(int buttonCode, int actionCode) {
- if (mVirtualMouse != null) {
- mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder()
- .setAction(actionCode)
- .setButtonCode(buttonCode)
- .build()
- );
- }
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder()
+ .setAction(actionCode)
+ .setButtonCode(buttonCode)
+ .build()
+ );
}
/**
@@ -205,12 +221,11 @@
case DOWN_MOVE_OR_SCROLL -> -1.0f;
default -> 0.0f;
};
- if (mVirtualMouse != null) {
- mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
- .setYAxisMovement(y)
- .build()
- );
- }
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
+ .setYAxisMovement(y)
+ .build()
+ );
if (DEBUG) {
Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
+ " for scroll action with axis movement (y=" + y + ")");
diff --git a/services/appfunctions/OWNERS b/services/appfunctions/OWNERS
new file mode 100644
index 0000000..b310894
--- /dev/null
+++ b/services/appfunctions/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/app/appfunctions/OWNERS
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/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
index 0fdd57d..dca1491 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
@@ -264,4 +264,11 @@
}
});
}
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
+ @Override
+ public int getSensorId() {
+ super.getSensorId_enforcePermission();
+ return mSensorId;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
index 8dc560b..caa2c1c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
@@ -293,4 +293,11 @@
}
});
}
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
+ @Override
+ public int getSensorId() {
+ super.getSensorId_enforcePermission();
+ return mSensorId;
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 4dbbfa2..c77b768 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -512,6 +512,9 @@
/**
* Gets the next input method and subtype from the given ones.
*
+ * <p>If the given input method and subtype are not found, this returns the most recent
+ * input method and subtype.</p>
+ *
* @param imi the input method to find the next value from.
* @param subtype the input method subtype to find the next value from, if any.
* @param onlyCurrentIme whether to consider only subtypes of the current input method.
@@ -523,17 +526,20 @@
public ImeSubtypeListItem next(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype, boolean onlyCurrentIme,
boolean useRecency, boolean forward) {
- final int size = mItems.size();
- if (size <= 1) {
+ if (mItems.isEmpty()) {
return null;
}
final int index = getIndex(imi, subtype, useRecency);
if (index < 0) {
- return null;
+ Slog.w(TAG, "Trying to switch away from input method: " + imi
+ + " and subtype " + subtype + " which are not in the list,"
+ + " falling back to most recent item in list.");
+ return mItems.get(mRecencyMap[0]);
}
final int incrementSign = (forward ? 1 : -1);
+ final int size = mItems.size();
for (int i = 1; i < size; i++) {
final int nextIndex = (index + i * incrementSign + size) % size;
final int mappedIndex = useRecency ? mRecencyMap[nextIndex] : nextIndex;
@@ -554,7 +560,7 @@
*/
public boolean setMostRecent(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype) {
- if (mItems.size() <= 1) {
+ if (mItems.isEmpty()) {
return false;
}
@@ -849,6 +855,9 @@
/**
* Gets the next input method and subtype, starting from the given ones, in the given direction.
*
+ * <p>If the given input method and subtype are not found, this returns the most recent
+ * input method and subtype.</p>
+ *
* @param onlyCurrentIme whether to consider only subtypes of the current input method.
* @param imi the input method to find the next value from.
* @param subtype the input method subtype to find the next value from, if any.
@@ -867,6 +876,9 @@
* Gets the next input method and subtype suitable for hardware keyboards, starting from the
* given ones, in the given direction.
*
+ * <p>If the given input method and subtype are not found, this returns the most recent
+ * input method and subtype.</p>
+ *
* @param onlyCurrentIme whether to consider only subtypes of the current input method.
* @param imi the input method to find the next value from.
* @param subtype the input method subtype to find the next value from, if any.
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 17f6561..a6f4c0e 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -530,6 +530,12 @@
*/
private boolean mUseDifferentDelaysForBackgroundChain;
+ /**
+ * Core uids and apps without the internet permission will not have any firewall rules applied
+ * to them.
+ */
+ private boolean mNeverApplyRulesToCoreUids;
+
// See main javadoc for instructions on how to use these locks.
final Object mUidRulesFirstLock = new Object();
final Object mNetworkPoliciesSecondLock = new Object();
@@ -760,7 +766,8 @@
/** List of apps indexed by uid and whether they have the internet permission */
@GuardedBy("mUidRulesFirstLock")
- private final SparseBooleanArray mInternetPermissionMap = new SparseBooleanArray();
+ @VisibleForTesting
+ final SparseBooleanArray mInternetPermissionMap = new SparseBooleanArray();
/**
* Map of uid -> UidStateCallbackInfo objects holding the data received from
@@ -1038,6 +1045,7 @@
mUseMeteredFirewallChains = Flags.useMeteredFirewallChains();
mUseDifferentDelaysForBackgroundChain = Flags.useDifferentDelaysForBackgroundChain();
+ mNeverApplyRulesToCoreUids = Flags.neverApplyRulesToCoreUids();
synchronized (mUidRulesFirstLock) {
synchronized (mNetworkPoliciesSecondLock) {
@@ -4088,6 +4096,8 @@
+ mUseMeteredFirewallChains);
fout.println(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN + ": "
+ mUseDifferentDelaysForBackgroundChain);
+ fout.println(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS + ": "
+ + mNeverApplyRulesToCoreUids);
fout.println();
fout.println("mRestrictBackgroundLowPowerMode: " + mRestrictBackgroundLowPowerMode);
@@ -4878,6 +4888,12 @@
int[] idleUids = mUsageStats.getIdleUidsForUser(user.id);
for (int uid : idleUids) {
if (!mPowerSaveTempWhitelistAppIds.get(UserHandle.getAppId(uid), false)) {
+ if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) {
+ // This check is needed to keep mUidFirewallStandbyRules free of any
+ // such uids. Doing this keeps it in sync with the actual rules applied
+ // in the underlying connectivity stack.
+ continue;
+ }
// quick check: if this uid doesn't have INTERNET permission, it
// doesn't have network access anyway, so it is a waste to mess
// with it here.
@@ -5180,6 +5196,11 @@
@GuardedBy("mUidRulesFirstLock")
private boolean isUidValidForAllowlistRulesUL(int uid) {
+ return isUidValidForRulesUL(uid);
+ }
+
+ @GuardedBy("mUidRulesFirstLock")
+ private boolean isUidValidForRulesUL(int uid) {
return UserHandle.isApp(uid) && hasInternetPermissionUL(uid);
}
@@ -6194,41 +6215,33 @@
}
}
- private void addSdkSandboxUidsIfNeeded(SparseIntArray uidRules) {
- final int size = uidRules.size();
- final SparseIntArray sdkSandboxUids = new SparseIntArray();
- for (int index = 0; index < size; index++) {
- final int uid = uidRules.keyAt(index);
- final int rule = uidRules.valueAt(index);
- if (Process.isApplicationUid(uid)) {
- sdkSandboxUids.put(Process.toSdkSandboxUid(uid), rule);
- }
- }
-
- for (int index = 0; index < sdkSandboxUids.size(); index++) {
- final int uid = sdkSandboxUids.keyAt(index);
- final int rule = sdkSandboxUids.valueAt(index);
- uidRules.put(uid, rule);
- }
- }
-
/**
* Set uid rules on a particular firewall chain. This is going to synchronize the rules given
* here to netd. It will clean up dead rules and make sure the target chain only contains rules
* specified here.
*/
+ @GuardedBy("mUidRulesFirstLock")
private void setUidFirewallRulesUL(int chain, SparseIntArray uidRules) {
- addSdkSandboxUidsIfNeeded(uidRules);
try {
int size = uidRules.size();
- int[] uids = new int[size];
- int[] rules = new int[size];
+ final IntArray uids = new IntArray(size);
+ final IntArray rules = new IntArray(size);
for(int index = size - 1; index >= 0; --index) {
- uids[index] = uidRules.keyAt(index);
- rules[index] = uidRules.valueAt(index);
+ final int uid = uidRules.keyAt(index);
+ if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) {
+ continue;
+ }
+ uids.add(uid);
+ rules.add(uidRules.valueAt(index));
+ if (Process.isApplicationUid(uid)) {
+ uids.add(Process.toSdkSandboxUid(uid));
+ rules.add(uidRules.valueAt(index));
+ }
}
- mNetworkManager.setFirewallUidRules(chain, uids, rules);
- mLogger.firewallRulesChanged(chain, uids, rules);
+ final int[] uidArray = uids.toArray();
+ final int[] ruleArray = rules.toArray();
+ mNetworkManager.setFirewallUidRules(chain, uidArray, ruleArray);
+ mLogger.firewallRulesChanged(chain, uidArray, ruleArray);
} catch (IllegalStateException e) {
Log.wtf(TAG, "problem setting firewall uid rules", e);
} catch (RemoteException e) {
@@ -6241,6 +6254,9 @@
*/
@GuardedBy("mUidRulesFirstLock")
private void setUidFirewallRuleUL(int chain, int uid, int rule) {
+ if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) {
+ return;
+ }
if (Trace.isTagEnabled(Trace.TRACE_TAG_NETWORK)) {
Trace.traceBegin(Trace.TRACE_TAG_NETWORK,
"setUidFirewallRuleUL: " + chain + "/" + uid + "/" + rule);
@@ -6249,8 +6265,6 @@
if (chain == FIREWALL_CHAIN_STANDBY) {
mUidFirewallStandbyRules.put(uid, rule);
}
- // Note that we do not need keep a separate cache of uid rules for chains that we do
- // not call #setUidFirewallRulesUL for.
try {
mNetworkManager.setFirewallUidRule(chain, uid, rule);
@@ -6295,6 +6309,8 @@
* Resets all firewall rules associated with an UID.
*/
private void resetUidFirewallRules(int uid) {
+ // Resetting rules for uids with isUidValidForRulesUL = false should be OK as no rules
+ // should be previously set and the downstream code will skip no-op changes.
try {
mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_DOZABLE, uid,
FIREWALL_RULE_DEFAULT);
diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig
index 586baf0..7f04e66 100644
--- a/services/core/java/com/android/server/net/flags.aconfig
+++ b/services/core/java/com/android/server/net/flags.aconfig
@@ -27,3 +27,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "never_apply_rules_to_core_uids"
+ namespace: "backstage_power"
+ description: "Removes all rule bookkeeping and evaluation logic for core uids and uids without the internet permission"
+ bug: "356956588"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
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/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index b4459cb..9818916 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -1519,7 +1519,14 @@
@Override
public void setLastNotificationUpdateTimeMs(NotificationRecord record,
long timestampMillis) {
- super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+ if (Flags.politeNotificationsAttnUpdate()) {
+ // Set last update per package/channel only for exempt notifications
+ if (isAvalancheExempted(record)) {
+ super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+ }
+ } else {
+ super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+ }
mLastNotificationTimestamp = timestampMillis;
mAppStrategy.setLastNotificationUpdateTimeMs(record, timestampMillis);
}
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/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 46585a5..6303ecd 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -80,6 +80,7 @@
import android.util.Slog;
import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.content.PackageMonitor;
import com.android.internal.content.om.OverlayConfig;
@@ -1180,6 +1181,7 @@
// intent, querying the PackageManagerService for the actual current
// state may lead to contradictions within OMS. Better then to lag
// behind until all pending intents have been processed.
+ @GuardedBy("itself")
private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>();
private final ArraySet<Integer> mInitializedUsers = new ArraySet<>();
@@ -1207,10 +1209,12 @@
}
final ArrayMap<String, PackageState> userPackages = new ArrayMap<>();
- for (int i = 0, n = mCache.size(); i < n; i++) {
- final PackageStateUsers pkg = mCache.valueAt(i);
- if (pkg.mInstalledUsers.contains(userId)) {
- userPackages.put(mCache.keyAt(i), pkg.mPackageState);
+ synchronized (mCache) {
+ for (int i = 0, n = mCache.size(); i < n; i++) {
+ final PackageStateUsers pkg = mCache.valueAt(i);
+ if (pkg.mInstalledUsers.contains(userId)) {
+ userPackages.put(mCache.keyAt(i), pkg.mPackageState);
+ }
}
}
return userPackages;
@@ -1220,7 +1224,11 @@
@Nullable
public PackageState getPackageStateForUser(@NonNull final String packageName,
final int userId) {
- final PackageStateUsers pkg = mCache.get(packageName);
+ final PackageStateUsers pkg;
+
+ synchronized (mCache) {
+ pkg = mCache.get(packageName);
+ }
if (pkg != null && pkg.mInstalledUsers.contains(userId)) {
return pkg.mPackageState;
}
@@ -1251,12 +1259,15 @@
@NonNull
private PackageState addPackageUser(@NonNull final PackageState pkg,
final int user) {
- PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName());
- if (pkgUsers == null) {
- pkgUsers = new PackageStateUsers(pkg);
- mCache.put(pkg.getPackageName(), pkgUsers);
- } else {
- pkgUsers.mPackageState = pkg;
+ PackageStateUsers pkgUsers;
+ synchronized (mCache) {
+ pkgUsers = mCache.get(pkg.getPackageName());
+ if (pkgUsers == null) {
+ pkgUsers = new PackageStateUsers(pkg);
+ mCache.put(pkg.getPackageName(), pkgUsers);
+ } else {
+ pkgUsers.mPackageState = pkg;
+ }
}
pkgUsers.mInstalledUsers.add(user);
return pkgUsers.mPackageState;
@@ -1265,18 +1276,24 @@
@NonNull
private void removePackageUser(@NonNull final String packageName, final int user) {
- final PackageStateUsers pkgUsers = mCache.get(packageName);
- if (pkgUsers == null) {
- return;
+ // synchronize should include the call to the other removePackageUser() method so that
+ // the access and modification happen under the same lock.
+ synchronized (mCache) {
+ final PackageStateUsers pkgUsers = mCache.get(packageName);
+ if (pkgUsers == null) {
+ return;
+ }
+ removePackageUser(pkgUsers, user);
}
- removePackageUser(pkgUsers, user);
}
@NonNull
private void removePackageUser(@NonNull final PackageStateUsers pkg, final int user) {
pkg.mInstalledUsers.remove(user);
if (pkg.mInstalledUsers.isEmpty()) {
- mCache.remove(pkg.mPackageState.getPackageName());
+ synchronized (mCache) {
+ mCache.remove(pkg.mPackageState.getPackageName());
+ }
}
}
@@ -1386,8 +1403,10 @@
public void forgetAllPackageInfos(final int userId) {
// Iterate in reverse order since removing the package in all users will remove the
// package from the cache.
- for (int i = mCache.size() - 1; i >= 0; i--) {
- removePackageUser(mCache.valueAt(i), userId);
+ synchronized (mCache) {
+ for (int i = mCache.size() - 1; i >= 0; i--) {
+ removePackageUser(mCache.valueAt(i), userId);
+ }
}
}
@@ -1405,22 +1424,23 @@
public void dump(@NonNull final PrintWriter pw, @NonNull DumpState dumpState) {
pw.println("AndroidPackage cache");
+ synchronized (mCache) {
+ if (!dumpState.isVerbose()) {
+ pw.println(TAB1 + mCache.size() + " package(s)");
+ return;
+ }
- if (!dumpState.isVerbose()) {
- pw.println(TAB1 + mCache.size() + " package(s)");
- return;
- }
+ if (mCache.size() == 0) {
+ pw.println(TAB1 + "<empty>");
+ return;
+ }
- if (mCache.size() == 0) {
- pw.println(TAB1 + "<empty>");
- return;
- }
-
- for (int i = 0, n = mCache.size(); i < n; i++) {
- final String packageName = mCache.keyAt(i);
- final PackageStateUsers pkg = mCache.valueAt(i);
- pw.print(TAB1 + packageName + ": " + pkg.mPackageState + " users=");
- pw.println(TextUtils.join(", ", pkg.mInstalledUsers));
+ for (int i = 0, n = mCache.size(); i < n; i++) {
+ final String packageName = mCache.keyAt(i);
+ final PackageStateUsers pkg = mCache.valueAt(i);
+ pw.print(TAB1 + packageName + ": " + pkg.mPackageState + " users=");
+ pw.println(TextUtils.join(", ", pkg.mInstalledUsers));
+ }
}
}
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 829ee27..57d7d79 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2105,6 +2105,10 @@
@Override
public void setUserAdmin(@UserIdInt int userId) {
checkManageUserAndAcrossUsersFullPermission("set user admin");
+ if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ checkAdminStatusChangeAllowed(userId);
+ }
+
mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_GRANT_ADMIN);
UserData user;
synchronized (mPackagesLock) {
@@ -2133,6 +2137,10 @@
@Override
public void revokeUserAdmin(@UserIdInt int userId) {
checkManageUserAndAcrossUsersFullPermission("revoke admin privileges");
+ if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ checkAdminStatusChangeAllowed(userId);
+ }
+
mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_REVOKE_ADMIN);
UserData user;
synchronized (mPackagesLock) {
@@ -4065,6 +4073,26 @@
}
}
+ /**
+ * Checks if changing the admin status of a target user is restricted
+ * due to the DISALLOW_GRANT_ADMIN restriction. If either the calling
+ * user or the target user has this restriction, a SecurityException
+ * is thrown.
+ *
+ * @param targetUser The user ID of the user whose admin status is being
+ * considered for change.
+ * @throws SecurityException if the admin status change is restricted due
+ * to the DISALLOW_GRANT_ADMIN restriction.
+ */
+ private void checkAdminStatusChangeAllowed(int targetUser) {
+ if (hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, UserHandle.getCallingUserId())
+ || hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, targetUser)) {
+ throw new SecurityException(
+ "Admin status change is restricted. The DISALLOW_GRANT_ADMIN "
+ + "restriction is applied either on the current or the target user.");
+ }
+ }
+
@GuardedBy({"mPackagesLock"})
private void writeBitmapLP(UserInfo info, Bitmap bitmap) {
try {
@@ -5443,6 +5471,13 @@
enforceUserRestriction(restriction, UserHandle.getCallingUserId(),
"Cannot add user");
+ if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ if ((flags & UserInfo.FLAG_ADMIN) != 0) {
+ enforceUserRestriction(UserManager.DISALLOW_GRANT_ADMIN,
+ UserHandle.getCallingUserId(), "Cannot create ADMIN user");
+ }
+ }
+
return createUserInternalUnchecked(name, userType, flags, parentId,
/* preCreate= */ false, disallowedPackages, /* token= */ null);
}
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/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 3933759..a74c4e0 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -17,7 +17,6 @@
package com.android.server.vibrator;
import android.annotation.NonNull;
-import android.content.Context;
import android.hardware.vibrator.V1_0.EffectStrength;
import android.os.ExternalVibrationScale;
import android.os.VibrationAttributes;
@@ -25,6 +24,7 @@
import android.os.Vibrator;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.VibrationConfig;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
@@ -37,8 +37,11 @@
final class VibrationScaler {
private static final String TAG = "VibrationScaler";
+ // TODO(b/345186129): remove this once we finish migrating to scale factor and clean up flags.
// Scale levels. Each level, except MUTE, is defined as the delta between the current setting
// and the default intensity for that type of vibration (i.e. current - default).
+ // It's important that we apply the scaling on the delta between the two so
+ // that the default intensity level applies no scaling to application provided effects.
static final int SCALE_VERY_LOW = ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW; // -2
static final int SCALE_LOW = ExternalVibrationScale.ScaleLevel.SCALE_LOW; // -1
static final int SCALE_NONE = ExternalVibrationScale.ScaleLevel.SCALE_NONE; // 0
@@ -53,35 +56,15 @@
private static final float SCALE_FACTOR_HIGH = 1.2f;
private static final float SCALE_FACTOR_VERY_HIGH = 1.4f;
- private static final ScaleLevel SCALE_LEVEL_NONE = new ScaleLevel(SCALE_FACTOR_NONE);
-
- // A mapping from the intensity adjustment to the scaling to apply, where the intensity
- // adjustment is defined as the delta between the default intensity level and the user selected
- // intensity level. It's important that we apply the scaling on the delta between the two so
- // that the default intensity level applies no scaling to application provided effects.
- private final SparseArray<ScaleLevel> mScaleLevels;
private final VibrationSettings mSettingsController;
private final int mDefaultVibrationAmplitude;
+ private final float mDefaultVibrationScaleLevelGain;
private final SparseArray<Float> mAdaptiveHapticsScales = new SparseArray<>();
- VibrationScaler(Context context, VibrationSettings settingsController) {
+ VibrationScaler(VibrationConfig config, VibrationSettings settingsController) {
mSettingsController = settingsController;
- mDefaultVibrationAmplitude = context.getResources().getInteger(
- com.android.internal.R.integer.config_defaultVibrationAmplitude);
-
- mScaleLevels = new SparseArray<>();
- mScaleLevels.put(SCALE_VERY_LOW, new ScaleLevel(SCALE_FACTOR_VERY_LOW));
- mScaleLevels.put(SCALE_LOW, new ScaleLevel(SCALE_FACTOR_LOW));
- mScaleLevels.put(SCALE_NONE, SCALE_LEVEL_NONE);
- mScaleLevels.put(SCALE_HIGH, new ScaleLevel(SCALE_FACTOR_HIGH));
- mScaleLevels.put(SCALE_VERY_HIGH, new ScaleLevel(SCALE_FACTOR_VERY_HIGH));
- }
-
- /**
- * Returns the default vibration amplitude configured for this device, value in [1,255].
- */
- public int getDefaultVibrationAmplitude() {
- return mDefaultVibrationAmplitude;
+ mDefaultVibrationAmplitude = config.getDefaultVibrationAmplitude();
+ mDefaultVibrationScaleLevelGain = config.getDefaultVibrationScaleLevelGain();
}
/**
@@ -111,6 +94,16 @@
}
/**
+ * Calculates the scale factor to be applied to a vibration with given usage.
+ *
+ * @param usageHint one of VibrationAttributes.USAGE_*
+ * @return The scale factor.
+ */
+ public float getScaleFactor(int usageHint) {
+ return scaleLevelToScaleFactor(getScaleLevel(usageHint));
+ }
+
+ /**
* Returns the adaptive haptics scale that should be applied to the vibrations with
* the given usage. When no adaptive scales are available for the usages, then returns 1
* indicating no scaling will be applied
@@ -135,20 +128,12 @@
@NonNull
public VibrationEffect scale(@NonNull VibrationEffect effect, int usageHint) {
int newEffectStrength = getEffectStrength(usageHint);
- ScaleLevel scaleLevel = mScaleLevels.get(getScaleLevel(usageHint));
+ float scaleFactor = getScaleFactor(usageHint);
float adaptiveScale = getAdaptiveHapticsScale(usageHint);
- if (scaleLevel == null) {
- // Something about our scaling has gone wrong, so just play with no scaling.
- Slog.e(TAG, "No configured scaling level found! (current="
- + mSettingsController.getCurrentIntensity(usageHint) + ", default= "
- + mSettingsController.getDefaultIntensity(usageHint) + ")");
- scaleLevel = SCALE_LEVEL_NONE;
- }
-
return effect.resolve(mDefaultVibrationAmplitude)
.applyEffectStrength(newEffectStrength)
- .scale(scaleLevel.factor)
+ .scale(scaleFactor)
.scaleLinearly(adaptiveScale);
}
@@ -192,14 +177,11 @@
void dump(IndentingPrintWriter pw) {
pw.println("VibrationScaler:");
pw.increaseIndent();
- pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude);
pw.println("ScaleLevels:");
pw.increaseIndent();
- for (int i = 0; i < mScaleLevels.size(); i++) {
- int scaleLevelKey = mScaleLevels.keyAt(i);
- ScaleLevel scaleLevel = mScaleLevels.valueAt(i);
- pw.println(scaleLevelToString(scaleLevelKey) + " = " + scaleLevel);
+ for (int level = SCALE_VERY_LOW; level <= SCALE_VERY_HIGH; level++) {
+ pw.println(scaleLevelToString(level) + " = " + scaleLevelToScaleFactor(level));
}
pw.decreaseIndent();
@@ -224,16 +206,24 @@
@Override
public String toString() {
+ StringBuilder scaleLevelsStr = new StringBuilder("{");
+ for (int level = SCALE_VERY_LOW; level <= SCALE_VERY_HIGH; level++) {
+ scaleLevelsStr.append(scaleLevelToString(level))
+ .append("=").append(scaleLevelToScaleFactor(level));
+ if (level < SCALE_FACTOR_VERY_HIGH) {
+ scaleLevelsStr.append(", ");
+ }
+ }
+ scaleLevelsStr.append("}");
+
return "VibrationScaler{"
- + "mScaleLevels=" + mScaleLevels
- + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude
+ + "mScaleLevels=" + scaleLevelsStr
+ ", mAdaptiveHapticsScales=" + mAdaptiveHapticsScales
+ '}';
}
private int getEffectStrength(int usageHint) {
int currentIntensity = mSettingsController.getCurrentIntensity(usageHint);
-
if (currentIntensity == Vibrator.VIBRATION_INTENSITY_OFF) {
// Bypassing user settings, or it has changed between checking and scaling. Use default.
currentIntensity = mSettingsController.getDefaultIntensity(usageHint);
@@ -244,17 +234,44 @@
/** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
private static int intensityToEffectStrength(int intensity) {
- switch (intensity) {
- case Vibrator.VIBRATION_INTENSITY_LOW:
- return EffectStrength.LIGHT;
- case Vibrator.VIBRATION_INTENSITY_MEDIUM:
- return EffectStrength.MEDIUM;
- case Vibrator.VIBRATION_INTENSITY_HIGH:
- return EffectStrength.STRONG;
- default:
+ return switch (intensity) {
+ case Vibrator.VIBRATION_INTENSITY_LOW -> EffectStrength.LIGHT;
+ case Vibrator.VIBRATION_INTENSITY_MEDIUM -> EffectStrength.MEDIUM;
+ case Vibrator.VIBRATION_INTENSITY_HIGH -> EffectStrength.STRONG;
+ default -> {
Slog.w(TAG, "Got unexpected vibration intensity: " + intensity);
- return EffectStrength.STRONG;
+ yield EffectStrength.STRONG;
+ }
+ };
+ }
+
+ /** Mapping of ExternalVibrationScale.ScaleLevel.SCALE_* values to scale factor. */
+ private float scaleLevelToScaleFactor(int level) {
+ if (Flags.hapticsScaleV2Enabled()) {
+ if (level == SCALE_NONE || level < SCALE_VERY_LOW || level > SCALE_VERY_HIGH) {
+ // Scale set to none or to a bad value, use default factor for no scaling.
+ return SCALE_FACTOR_NONE;
+ }
+ float scaleFactor = (float) Math.pow(mDefaultVibrationScaleLevelGain, level);
+ if (scaleFactor <= 0) {
+ // Something about our scaling has gone wrong, so just play with no scaling.
+ Slog.wtf(TAG, String.format(Locale.ROOT, "Error in scaling calculations, ended up"
+ + " with invalid scale factor %.2f for scale level %s and default"
+ + " level gain of %.2f", scaleFactor, scaleLevelToString(level),
+ mDefaultVibrationScaleLevelGain));
+ scaleFactor = SCALE_FACTOR_NONE;
+ }
+ return scaleFactor;
}
+
+ return switch (level) {
+ case SCALE_VERY_LOW -> SCALE_FACTOR_VERY_LOW;
+ case SCALE_LOW -> SCALE_FACTOR_LOW;
+ case SCALE_HIGH -> SCALE_FACTOR_HIGH;
+ case SCALE_VERY_HIGH -> SCALE_FACTOR_VERY_HIGH;
+ // Scale set to none or to a bad value, use default factor for no scaling.
+ default -> SCALE_FACTOR_NONE;
+ };
}
static String scaleLevelToString(int scaleLevel) {
@@ -267,18 +284,4 @@
default -> String.valueOf(scaleLevel);
};
}
-
- /** Represents the scale that must be applied to a vibration effect intensity. */
- private static final class ScaleLevel {
- public final float factor;
-
- ScaleLevel(float factor) {
- this.factor = factor;
- }
-
- @Override
- public String toString() {
- return "ScaleLevel{factor=" + factor + "}";
- }
- }
}
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/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 76872cf..f2ad5b9 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -57,6 +57,7 @@
import android.os.VibratorInfo;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
import android.os.vibrator.VibratorInfoFactory;
import android.os.vibrator.persistence.ParsedVibration;
@@ -251,8 +252,9 @@
mHandler = injector.createHandler(Looper.myLooper());
mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler);
- mVibrationSettings = new VibrationSettings(mContext, mHandler);
- mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings);
+ VibrationConfig vibrationConfig = new VibrationConfig(context.getResources());
+ mVibrationSettings = new VibrationSettings(mContext, mHandler, vibrationConfig);
+ mVibrationScaler = new VibrationScaler(vibrationConfig, mVibrationSettings);
mVibratorControlService = new VibratorControlService(mContext,
injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings,
mFrameworkStatsLogger, mLock);
@@ -1698,7 +1700,7 @@
IBinder.DeathRecipient {
public final ExternalVibration externalVibration;
- public ExternalVibrationScale scale = new ExternalVibrationScale();
+ public final ExternalVibrationScale scale = new ExternalVibrationScale();
private Vibration.Status mStatus;
@@ -1712,8 +1714,18 @@
mStatus = Vibration.Status.RUNNING;
}
+ public void muteScale() {
+ scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ if (Flags.hapticsScaleV2Enabled()) {
+ scale.scaleFactor = 0;
+ }
+ }
+
public void scale(VibrationScaler scaler, int usage) {
scale.scaleLevel = scaler.getScaleLevel(usage);
+ if (Flags.hapticsScaleV2Enabled()) {
+ scale.scaleFactor = scaler.getScaleFactor(usage);
+ }
scale.adaptiveHapticsScale = scaler.getAdaptiveHapticsScale(usage);
stats.reportAdaptiveScale(scale.adaptiveHapticsScale);
}
@@ -2047,7 +2059,7 @@
// Create Vibration.Stats as close to the received request as possible, for tracking.
ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
// Mute the request until we run all the checks and accept the vibration.
- vibHolder.scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ vibHolder.muteScale();
boolean alreadyUnderExternalControl = false;
boolean waitForCompletion = false;
@@ -2146,7 +2158,7 @@
new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING),
/* continueExternalControl= */ false);
// Mute the request, vibration will be ignored.
- vibHolder.scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ vibHolder.muteScale();
}
return vibHolder.scale;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index a22db97..5c096ec 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2524,8 +2524,11 @@
// trampoline that will be always created and finished immediately. Then give a chance to
// see if the snapshot is usable for the current running activity so the transition will
// look smoother, instead of showing a splash screen on the second launch.
- if (!newTask && taskSwitch && processRunning && !activityCreated && task.intent != null
- && mActivityComponent.equals(task.intent.getComponent())) {
+ if (!newTask && taskSwitch && !activityCreated && task.intent != null
+ // Another case where snapshot is allowed to be used is if this activity has not yet
+ // been created && is translucent or floating.
+ // The component isn't necessary to be matched in this case.
+ && (!mOccludesParent || mActivityComponent.equals(task.intent.getComponent()))) {
final ActivityRecord topAttached = task.getActivity(ActivityRecord::attachedToProcess);
if (topAttached != null) {
if (topAttached.isSnapshotCompatible(snapshot)
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/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index b4c7557..fe5b142 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -761,7 +761,7 @@
if (isMonitorForRemote()) {
mObserver.sendResult(null /* result */);
}
- if (isMonitorAnimationOrTransition()) {
+ if (isMonitorAnimationOrTransition() && canCancelAnimations()) {
clearBackAnimations(true /* cancel */);
}
cancelPendingAnimation();
@@ -1935,8 +1935,7 @@
for (int i = penActivities.length - 1; i >= 0; --i) {
ActivityRecord resetActivity = penActivities[i];
if (transition.isInTransition(resetActivity)) {
- resetActivity.mTransitionController.setReady(
- resetActivity.getDisplayContent(), true);
+ transition.setReady(resetActivity.getDisplayContent(), true);
return true;
}
}
@@ -1991,18 +1990,12 @@
activity.makeVisibleIfNeeded(null /* starting */, true /* notifyToClient */);
}
}
- boolean needTransition = false;
- final DisplayContent dc = affects.get(0).getDisplayContent();
- for (int i = affects.size() - 1; i >= 0; --i) {
- final ActivityRecord activity = affects.get(i);
- needTransition |= tc.isCollecting(activity);
- }
if (prepareOpen != null) {
- if (needTransition) {
+ if (prepareOpen.hasChanges()) {
tc.requestStartTransition(prepareOpen,
null /*startTask */, null /* remoteTransition */,
null /* displayChange */);
- tc.setReady(dc);
+ prepareOpen.setReady(affects.get(0), true);
return prepareOpen;
} else {
prepareOpen.abort();
@@ -2053,11 +2046,22 @@
}
}
+ /** If the open transition is playing, wait for transition to clear the animation */
+ private boolean canCancelAnimations() {
+ if (!Flags.migratePredictiveBackTransition()) {
+ return true;
+ }
+ return mAnimationHandler.mOpenAnimAdaptor == null
+ || mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition == null;
+ }
+
void startAnimation() {
if (!mBackAnimationInProgress) {
// gesture is already finished, do not start animation
if (mPendingAnimation != null) {
- clearBackAnimations(true /* cancel */);
+ if (canCancelAnimations()) {
+ clearBackAnimations(true /* cancel */);
+ }
mPendingAnimation = null;
}
return;
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 7a95c2d..2f0ee17 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -129,21 +129,33 @@
@WindowConfiguration.WindowingMode
private int getWindowingModeLocked(@NonNull SettingsProvider.SettingsEntry settings,
@NonNull DisplayContent dc) {
- int windowingMode = settings.mWindowingMode;
+ final int windowingModeFromDisplaySettings = settings.mWindowingMode;
// This display used to be in freeform, but we don't support freeform anymore, so fall
// back to fullscreen.
- if (windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
+ if (windowingModeFromDisplaySettings == WindowConfiguration.WINDOWING_MODE_FREEFORM
&& !mService.mAtmService.mSupportsFreeformWindowManagement) {
return WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
}
- // No record is present so use default windowing mode policy.
- if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
- windowingMode = mService.mAtmService.mSupportsFreeformWindowManagement
- && (mService.mIsPc || dc.forceDesktopMode())
- ? WindowConfiguration.WINDOWING_MODE_FREEFORM
- : WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+ if (windowingModeFromDisplaySettings != WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
+ return windowingModeFromDisplaySettings;
}
- return windowingMode;
+ // No record is present so use default windowing mode policy.
+ final boolean forceFreeForm = mService.mAtmService.mSupportsFreeformWindowManagement
+ && (mService.mIsPc || dc.forceDesktopMode());
+ if (forceFreeForm) {
+ return WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ }
+ final int currentWindowingMode = dc.getDefaultTaskDisplayArea().getWindowingMode();
+ if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
+ // No record preset in settings + no mode set via the display area policy.
+ // Move to fullscreen as a fallback.
+ return WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+ }
+ if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) {
+ // Freeform was enabled before but disabled now, the TDA should now move to fullscreen.
+ return WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+ }
+ return currentWindowingMode;
}
@WindowConfiguration.WindowingMode
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 2c1a333..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;
@@ -78,6 +79,8 @@
private static final int DEFER_WAKE_TRANSITION_TIMEOUT_MS = 5000;
+ private static final int GOING_AWAY_TIMEOUT_MS = 10500;
+
private final ActivityTaskSupervisor mTaskSupervisor;
private WindowManagerService mWindowManager;
@@ -233,6 +236,7 @@
dc.mWallpaperController.adjustWallpaperWindows();
dc.executeAppTransition();
}
+ scheduleGoingAwayTimeout(displayId);
}
// Update the sleep token first such that ensureActivitiesVisible has correct sleep token
@@ -287,6 +291,8 @@
mRootWindowContainer.ensureActivitiesVisible();
mRootWindowContainer.addStartingWindowsForVisibleActivities();
mWindowManager.executeAppTransition();
+
+ scheduleGoingAwayTimeout(displayId);
} finally {
mService.continueWindowLayout();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -602,6 +608,35 @@
}
}
+ /**
+ * Called when the default display's mKeyguardGoingAway has been left as {@code true} for too
+ * long. Send an explicit message to the KeyguardService asking it to wrap up.
+ */
+ private final Runnable mGoingAwayTimeout = () -> {
+ synchronized (mWindowManager.mGlobalLock) {
+ KeyguardDisplayState state = getDisplayState(DEFAULT_DISPLAY);
+ if (!state.mKeyguardGoingAway) {
+ return;
+ }
+ state.mKeyguardGoingAway = false;
+ state.writeEventLog("goingAwayTimeout");
+ mWindowManager.mPolicy.startKeyguardExitAnimation(
+ SystemClock.uptimeMillis() - GOING_AWAY_TIMEOUT_MS);
+ }
+ };
+
+ private void scheduleGoingAwayTimeout(int displayId) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ if (getDisplayState(displayId).mKeyguardGoingAway) {
+ if (!mWindowManager.mH.hasCallbacks(mGoingAwayTimeout)) {
+ mWindowManager.mH.postDelayed(mGoingAwayTimeout, GOING_AWAY_TIMEOUT_MS);
+ }
+ } else {
+ mWindowManager.mH.removeCallbacks(mGoingAwayTimeout);
+ }
+ }
/** Represents Keyguard state per individual display. */
private static class KeyguardDisplayState {
@@ -721,6 +756,7 @@
if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
writeEventLog("dismissIfInsecure");
controller.handleDismissInsecureKeyguard(display);
+ controller.scheduleGoingAwayTimeout(mDisplayId);
hasChange = true;
} else if (lastOccluded != mOccluded) {
controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
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/Task.java b/services/core/java/com/android/server/wm/Task.java
index d3df5fd..12e91ad 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3507,7 +3507,7 @@
| StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS);
if ((info.startingWindowTypeParameter
& StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED) != 0) {
- final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION);
+ final WindowState topMainWin = getTopFullscreenMainWindow();
if (topMainWin != null) {
info.mainWindowLayoutParams = topMainWin.getAttrs();
info.requestedVisibleTypes = topMainWin.getRequestedVisibleTypes();
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 3139e18..486a61b 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -3354,6 +3354,15 @@
return chg.hasChanged();
}
+ boolean hasChanges() {
+ for (int i = 0; i < mParticipants.size(); ++i) {
+ if (mChanges.get(mParticipants.valueAt(i)).hasChanged()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@VisibleForTesting
static class ChangeInfo {
private static final int FLAG_NONE = 0;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index 669a999..a08af72 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -2080,10 +2080,14 @@
String tag = parser.getName();
switch (tag) {
case TAG_LOCAL_POLICY_ENTRY:
- readLocalPoliciesInner(parser);
+ int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
+ if (!mLocalPolicies.contains(userId)) {
+ mLocalPolicies.put(userId, new HashMap<>());
+ }
+ readPoliciesInner(parser, mLocalPolicies.get(userId));
break;
case TAG_GLOBAL_POLICY_ENTRY:
- readGlobalPoliciesInner(parser);
+ readPoliciesInner(parser, mGlobalPolicies);
break;
case TAG_ENFORCING_ADMINS_ENTRY:
readEnforcingAdminsInner(parser);
@@ -2100,64 +2104,45 @@
}
}
- private void readLocalPoliciesInner(TypedXmlPullParser parser)
- throws XmlPullParserException, IOException {
- int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
- PolicyKey policyKey = null;
- PolicyState<?> policyState = null;
- int outerDepth = parser.getDepth();
- while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- String tag = parser.getName();
- switch (tag) {
- case TAG_POLICY_KEY_ENTRY:
- policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
- break;
- case TAG_POLICY_STATE_ENTRY:
- policyState = PolicyState.readFromXml(parser);
- break;
- default:
- Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag);
- }
- }
-
- if (policyKey != null && policyState != null) {
- if (!mLocalPolicies.contains(userId)) {
- mLocalPolicies.put(userId, new HashMap<>());
- }
- mLocalPolicies.get(userId).put(policyKey, policyState);
- } else {
- Slogf.wtf(TAG, "Error parsing local policy, policyKey is "
- + (policyKey == null ? "null" : policyKey) + ", and policyState is "
- + (policyState == null ? "null" : policyState) + ".");
- }
- }
-
- private void readGlobalPoliciesInner(TypedXmlPullParser parser)
+ private static void readPoliciesInner(
+ TypedXmlPullParser parser, Map<PolicyKey, PolicyState<?>> policyStateMap)
throws IOException, XmlPullParserException {
PolicyKey policyKey = null;
+ PolicyDefinition<?> policyDefinition = null;
PolicyState<?> policyState = null;
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
String tag = parser.getName();
switch (tag) {
case TAG_POLICY_KEY_ENTRY:
- policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
+ if (Flags.dontReadPolicyDefinition()) {
+ policyDefinition = PolicyDefinition.readFromXml(parser);
+ if (policyDefinition != null) {
+ policyKey = policyDefinition.getPolicyKey();
+ }
+ } else {
+ policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
+ }
break;
case TAG_POLICY_STATE_ENTRY:
- policyState = PolicyState.readFromXml(parser);
+ if (Flags.dontReadPolicyDefinition() && policyDefinition == null) {
+ Slogf.w(TAG, "Skipping policy state - unknown policy definition");
+ } else {
+ policyState = PolicyState.readFromXml(policyDefinition, parser);
+ }
break;
default:
- Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag);
+ Slogf.wtf(TAG, "Unknown tag for policy entry" + tag);
}
}
- if (policyKey != null && policyState != null) {
- mGlobalPolicies.put(policyKey, policyState);
- } else {
- Slogf.wtf(TAG, "Error parsing global policy, policyKey is "
- + (policyKey == null ? "null" : policyKey) + ", and policyState is "
- + (policyState == null ? "null" : policyState) + ".");
+ if (policyKey == null || policyState == null) {
+ Slogf.wtf(TAG, "Error parsing policy, policyKey is %s, and policyState is %s.",
+ policyKey, policyState);
+ return;
}
+
+ policyStateMap.put(policyKey, policyState);
}
private void readEnforcingAdminsInner(TypedXmlPullParser parser)
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index f86d307..19a942c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -129,9 +129,8 @@
*/
static PolicyDefinition<Integer> PERMISSION_GRANT(
@NonNull String packageName, @NonNull String permissionName) {
- if (packageName == null || permissionName == null) {
- return GENERIC_PERMISSION_GRANT;
- }
+ Objects.requireNonNull(packageName, "packageName must not be null");
+ Objects.requireNonNull(permissionName, "permissionName must not be null");
return GENERIC_PERMISSION_GRANT.createPolicyDefinition(
new PackagePermissionPolicyKey(
DevicePolicyIdentifiers.PERMISSION_GRANT_POLICY,
@@ -190,10 +189,8 @@
* {@link #GENERIC_PERSISTENT_PREFERRED_ACTIVITY}.
*/
static PolicyDefinition<ComponentName> PERSISTENT_PREFERRED_ACTIVITY(
- IntentFilter intentFilter) {
- if (intentFilter == null) {
- return GENERIC_PERSISTENT_PREFERRED_ACTIVITY;
- }
+ @NonNull IntentFilter intentFilter) {
+ Objects.requireNonNull(intentFilter, "intentFilter must not be null");
return GENERIC_PERSISTENT_PREFERRED_ACTIVITY.createPolicyDefinition(
new IntentFilterPolicyKey(
DevicePolicyIdentifiers.PERSISTENT_PREFERRED_ACTIVITY_POLICY,
@@ -216,11 +213,8 @@
* Passing in {@code null} for {@code packageName} will return
* {@link #GENERIC_PACKAGE_UNINSTALL_BLOCKED}.
*/
- static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED(
- String packageName) {
- if (packageName == null) {
- return GENERIC_PACKAGE_UNINSTALL_BLOCKED;
- }
+ static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
return GENERIC_PACKAGE_UNINSTALL_BLOCKED.createPolicyDefinition(
new PackagePolicyKey(
DevicePolicyIdentifiers.PACKAGE_UNINSTALL_BLOCKED_POLICY, packageName));
@@ -247,10 +241,8 @@
* Passing in {@code null} for {@code packageName} will return
* {@link #GENERIC_APPLICATION_RESTRICTIONS}.
*/
- static PolicyDefinition<Bundle> APPLICATION_RESTRICTIONS(String packageName) {
- if (packageName == null) {
- return GENERIC_APPLICATION_RESTRICTIONS;
- }
+ static PolicyDefinition<Bundle> APPLICATION_RESTRICTIONS(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
return GENERIC_APPLICATION_RESTRICTIONS.createPolicyDefinition(
new PackagePolicyKey(
DevicePolicyIdentifiers.APPLICATION_RESTRICTIONS_POLICY, packageName));
@@ -293,10 +285,8 @@
* Passing in {@code null} for {@code packageName} will return
* {@link #GENERIC_APPLICATION_HIDDEN}.
*/
- static PolicyDefinition<Boolean> APPLICATION_HIDDEN(String packageName) {
- if (packageName == null) {
- return GENERIC_APPLICATION_HIDDEN;
- }
+ static PolicyDefinition<Boolean> APPLICATION_HIDDEN(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
return GENERIC_APPLICATION_HIDDEN.createPolicyDefinition(
new PackagePolicyKey(
DevicePolicyIdentifiers.APPLICATION_HIDDEN_POLICY, packageName));
@@ -319,10 +309,8 @@
* Passing in {@code null} for {@code accountType} will return
* {@link #GENERIC_ACCOUNT_MANAGEMENT_DISABLED}.
*/
- static PolicyDefinition<Boolean> ACCOUNT_MANAGEMENT_DISABLED(String accountType) {
- if (accountType == null) {
- return GENERIC_ACCOUNT_MANAGEMENT_DISABLED;
- }
+ static PolicyDefinition<Boolean> ACCOUNT_MANAGEMENT_DISABLED(@NonNull String accountType) {
+ Objects.requireNonNull(accountType, "accountType must not be null");
return GENERIC_ACCOUNT_MANAGEMENT_DISABLED.createPolicyDefinition(
new AccountTypePolicyKey(
DevicePolicyIdentifiers.ACCOUNT_MANAGEMENT_DISABLED_POLICY, accountType));
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
index 245c438..b813489 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.PolicyValue;
+import android.app.admin.flags.Flags;
import android.util.IndentingPrintWriter;
import com.android.internal.util.XmlUtils;
@@ -254,11 +255,9 @@
}
@Nullable
- static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser)
+ static <V> PolicyState<V> readFromXml(
+ PolicyDefinition<V> policyDefinition, TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
-
- PolicyDefinition<V> policyDefinition = null;
-
PolicyValue<V> currentResolvedPolicy = null;
LinkedHashMap<EnforcingAdmin, PolicyValue<V>> policiesSetByAdmins = new LinkedHashMap<>();
@@ -300,10 +299,15 @@
}
break;
case TAG_POLICY_DEFINITION_ENTRY:
- policyDefinition = PolicyDefinition.readFromXml(parser);
- if (policyDefinition == null) {
- Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, "
- + "PolicyDefinition is null");
+ if (Flags.dontReadPolicyDefinition()) {
+ // Should be passed by the caller.
+ Objects.requireNonNull(policyDefinition);
+ } else {
+ policyDefinition = PolicyDefinition.readFromXml(parser);
+ if (policyDefinition == null) {
+ Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, "
+ + "PolicyDefinition is null");
+ }
}
break;
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index aee7242..a27ad9a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -913,52 +913,100 @@
final var controller = ControllerImpl.createFrom(null /* currentInstance */, List.of(),
List.of());
- assertNoAction(controller, false /* forHardware */, items);
- assertNoAction(controller, true /* forHardware */, hardwareItems);
+ assertNextItemNoAction(controller, false /* forHardware */, items,
+ null /* expectedNext */);
+ assertNextItemNoAction(controller, true /* forHardware */, hardwareItems,
+ null /* expectedNext */);
}
- /** Verifies that a controller with a single item can't take any actions. */
+ /**
+ * Verifies that a controller with a single item can't update the recency, and cannot switch
+ * away from the item, but allows switching from unknown items to the single item.
+ */
@RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@Test
public void testSingleItemList() {
final var items = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ null, true /* supportsSwitchingToNextInputMethod */);
+ final var unknownItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ null, true /* supportsSwitchingToNextInputMethod */);
+ final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var controller = ControllerImpl.createFrom(null /* currentInstance */,
- List.of(items.get(0)), List.of(hardwareItems.get(0)));
+ items, hardwareItems);
- assertNoAction(controller, false /* forHardware */, items);
- assertNoAction(controller, true /* forHardware */, hardwareItems);
+ assertNextItemNoAction(controller, false /* forHardware */, items,
+ null /* expectedNext */);
+ assertNextItemNoAction(controller, false /* forHardware */, unknownItems,
+ items.get(0));
+ assertNextItemNoAction(controller, true /* forHardware */, hardwareItems,
+ null /* expectedNext */);
+ assertNextItemNoAction(controller, true /* forHardware */, unknownHardwareItems,
+ hardwareItems.get(0));
}
- /** Verifies that a controller can't take any actions for unknown items. */
+ /**
+ * Verifies that the recency cannot be updated for unknown items, but switching from unknown
+ * items reaches the most recent known item.
+ */
@RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@Test
public void testUnknownItems() {
final var items = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var english = items.get(0);
+ final var french = items.get(1);
+ final var italian = items.get(2);
+
final var unknownItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
hardwareItems);
- assertNoAction(controller, false /* forHardware */, unknownItems);
- assertNoAction(controller, true /* forHardware */, unknownHardwareItems);
+ assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+ final var recencyItems = List.of(french, english, italian);
+
+ assertNextItemNoAction(controller, false /* forHardware */, unknownItems,
+ french);
+ assertNextItemNoAction(controller, true /* forHardware */, unknownHardwareItems,
+ hardwareItems.get(0));
+
+ // Known items must not be able to switch to unknown items.
+ assertNextOrder(controller, false /* forHardware */, MODE_STATIC, items,
+ List.of(items));
+ assertNextOrder(controller, false /* forHardware */, MODE_RECENT, recencyItems,
+ List.of(recencyItems));
+ assertNextOrder(controller, false /* forHardware */, MODE_AUTO, true /* forward */,
+ recencyItems, List.of(recencyItems));
+ assertNextOrder(controller, false /* forHardware */, MODE_AUTO, false /* forward */,
+ items.reversed(), List.of(items.reversed()));
+
+ assertNextOrder(controller, true /* forHardware */, MODE_STATIC, hardwareItems,
+ List.of(hardwareItems));
+ assertNextOrder(controller, true /* forHardware */, MODE_RECENT, hardwareItems,
+ List.of(hardwareItems));
+ assertNextOrder(controller, true /* forHardware */, MODE_AUTO, hardwareItems,
+ List.of(hardwareItems));
}
/** Verifies that the IME name does influence the comparison order. */
@@ -1199,25 +1247,26 @@
}
/**
- * Verifies that no next items can be found, and the recency cannot be updated for the
+ * Verifies that the expected next item is returned, and the recency cannot be updated for the
* given items.
*
- * @param controller the controller to verify the items on.
- * @param forHardware whether to try finding the next hardware item, or software item.
- * @param items the list of items to verify.
+ * @param controller the controller to verify the items on.
+ * @param forHardware whether to try finding the next hardware item, or software item.
+ * @param items the list of items to verify.
+ * @param expectedNext the expected next item.
*/
- private void assertNoAction(@NonNull ControllerImpl controller, boolean forHardware,
- @NonNull List<ImeSubtypeListItem> items) {
+ private void assertNextItemNoAction(@NonNull ControllerImpl controller, boolean forHardware,
+ @NonNull List<ImeSubtypeListItem> items, @Nullable ImeSubtypeListItem expectedNext) {
for (var item : items) {
for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) {
assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
- false /* forward */, item, null /* expectedNext */);
+ false /* forward */, item, expectedNext);
assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
- true /* forward */, item, null /* expectedNext */);
+ true /* forward */, item, expectedNext);
assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
- false /* forward */, item, null /* expectedNext */);
+ false /* forward */, item, expectedNext);
assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
- true /* forward */, item, null /* expectedNext */);
+ true /* forward */, item, expectedNext);
}
assertFalse("User action shouldn't have updated the recency.",
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index b980ca0..30de0e8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -512,6 +512,9 @@
when(mPermissionManagerInternal.getAppOpPermissionPackages(
SCHEDULE_EXACT_ALARM)).thenReturn(EmptyArray.STRING);
+ // Initialize timestamps with arbitrary values of time
+ mNowElapsedTest = 12;
+ mNowRtcTest = 345;
mInjector = new Injector(mMockContext);
mService = new AlarmManagerService(mMockContext, mInjector);
spyOn(mService);
@@ -774,6 +777,61 @@
}
@Test
+ public void timeChangeBroadcastForward() throws Exception {
+ final long timeDelta = 12345;
+ // AlarmManagerService sends the broadcast if real time clock proceeds 1000ms more than boot
+ // time clock.
+ mNowRtcTest += timeDelta + 1001;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ verify(mMockContext)
+ .sendBroadcastAsUser(
+ argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED),
+ eq(UserHandle.ALL),
+ isNull(),
+ any());
+ }
+
+ @Test
+ public void timeChangeBroadcastBackward() throws Exception {
+ final long timeDelta = 12345;
+ // AlarmManagerService sends the broadcast if real time clock proceeds 1000ms less than boot
+ // time clock.
+ mNowRtcTest += timeDelta - 1001;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ verify(mMockContext)
+ .sendBroadcastAsUser(
+ argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED),
+ eq(UserHandle.ALL),
+ isNull(),
+ any());
+ }
+
+ @Test
+ public void timeChangeFilterMinorAdjustment() throws Exception {
+ final long timeDelta = 12345;
+ // AlarmManagerService does not send the broadcast if real time clock proceeds within 1000ms
+ // than boot time clock.
+ mNowRtcTest += timeDelta + 1000;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ mNowRtcTest += timeDelta - 1000;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ verify(mMockContext, never())
+ .sendBroadcastAsUser(
+ argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED),
+ any(),
+ any(),
+ any());
+ }
+
+ @Test
public void testSingleAlarmExpiration() throws Exception {
final long triggerTime = mNowElapsedTest + 5000;
final PendingIntent alarmPi = getNewMockPendingIntent();
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
index 0def516..8753b25 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
@@ -45,7 +45,6 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
-import java.util.concurrent.TimeUnit
import java.util.LinkedList
import java.util.Queue
import android.util.ArraySet
@@ -117,9 +116,6 @@
Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager)
mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID)
- // VirtualMouse is created on a separate thread.
- // Wait for VirtualMouse to be created before running tests
- TimeUnit.MILLISECONDS.sleep(20L)
mouseKeysInterceptor.next = nextInterceptor
}
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/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 3d68849..dddab65 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -108,6 +108,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
@@ -165,6 +166,7 @@
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
+import android.os.Process;
import android.os.RemoteException;
import android.os.SimpleClock;
import android.os.SystemClock;
@@ -197,6 +199,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.BroadcastInterceptingContext.FutureIntent;
import com.android.internal.util.test.FsUtil;
@@ -2310,6 +2313,70 @@
assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
}
+ @SuppressWarnings("GuardedBy") // For not holding mUidRulesFirstLock
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS)
+ public void testRulesNeverAppliedToCoreUids() throws Exception {
+ clearInvocations(mNetworkManager);
+
+ final int coreAppId = Process.FIRST_APPLICATION_UID - 102;
+ final int coreUid = UserHandle.getUid(USER_ID, coreAppId);
+
+ // Enable all restrictions and add this core uid to all allowlists.
+ mService.mDeviceIdleMode = true;
+ mService.mRestrictPower = true;
+ setRestrictBackground(true);
+ expectHasUseRestrictedNetworksPermission(coreUid, true);
+ enableRestrictedMode(true);
+ final NetworkPolicyManagerInternal internal = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+ internal.setLowPowerStandbyActive(true);
+ internal.setLowPowerStandbyAllowlist(new int[]{coreUid});
+ internal.onTempPowerSaveWhitelistChange(coreAppId, true, REASON_OTHER, "testing");
+
+ when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean()))
+ .thenReturn(new int[]{coreAppId});
+ mPowerAllowlistReceiver.onReceive(mServiceContext, null);
+
+ // A normal uid would undergo a rule change from denied to allowed on all chains, but we
+ // should not request any rule change for this core uid.
+ verify(mNetworkManager, never()).setFirewallUidRule(anyInt(), eq(coreUid), anyInt());
+ verify(mNetworkManager, never()).setFirewallUidRules(anyInt(),
+ argThat(ar -> ArrayUtils.contains(ar, coreUid)), any(int[].class));
+ }
+
+ @SuppressWarnings("GuardedBy") // For not holding mUidRulesFirstLock
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS)
+ public void testRulesNeverAppliedToUidsWithoutInternetPermission() throws Exception {
+ clearInvocations(mNetworkManager);
+
+ mService.mInternetPermissionMap.clear();
+ expectHasInternetPermission(UID_A, false);
+
+ // Enable all restrictions and add this uid to all allowlists.
+ mService.mDeviceIdleMode = true;
+ mService.mRestrictPower = true;
+ setRestrictBackground(true);
+ expectHasUseRestrictedNetworksPermission(UID_A, true);
+ enableRestrictedMode(true);
+ final NetworkPolicyManagerInternal internal = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+ internal.setLowPowerStandbyActive(true);
+ internal.setLowPowerStandbyAllowlist(new int[]{UID_A});
+ internal.onTempPowerSaveWhitelistChange(APP_ID_A, true, REASON_OTHER, "testing");
+
+ when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean()))
+ .thenReturn(new int[]{APP_ID_A});
+ mPowerAllowlistReceiver.onReceive(mServiceContext, null);
+
+ // A normal uid would undergo a rule change from denied to allowed on all chains, but we
+ // should not request any rule this uid without the INTERNET permission.
+ verify(mNetworkManager, never()).setFirewallUidRule(anyInt(), eq(UID_A), anyInt());
+ verify(mNetworkManager, never()).setFirewallUidRules(anyInt(),
+ argThat(ar -> ArrayUtils.contains(ar, UID_A)), any(int[].class));
+ }
+
private boolean isUidState(int uid, int procState, int procStateSeq, int capability) {
final NetworkPolicyManager.UidState uidState = mService.getUidStateForTest(uid);
if (uidState == null) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index d714db99..7912156 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -121,6 +121,9 @@
// Making a copy of mUsersToRemove to avoid ConcurrentModificationException
mUsersToRemove.stream().toList().forEach(this::removeUser);
mUserRemovalWaiter.close();
+
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ mContext.getUser());
}
private void removeExistingUsers() {
@@ -935,6 +938,35 @@
@MediumTest
@Test
+ @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY)
+ public void testSetUserAdminThrowsSecurityException() throws Exception {
+ UserInfo targetUser = createUser("SecondaryUser", /*flags=*/ 0);
+ assertThat(targetUser.isAdmin()).isFalse();
+
+ try {
+ // 1. Target User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ targetUser.getUserHandle());
+ assertThrows(SecurityException.class, () -> mUserManager.setUserAdmin(targetUser.id));
+
+ // 2. Current User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ mContext.getUser());
+ assertThrows(SecurityException.class, () -> mUserManager.setUserAdmin(targetUser.id));
+
+ } finally {
+ // Ensure restriction is removed even if test fails
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ mContext.getUser());
+ }
+ }
+
+ @MediumTest
+ @Test
public void testRevokeUserAdmin() throws Exception {
UserInfo userInfo = createUser("Admin", /*flags=*/ UserInfo.FLAG_ADMIN);
assertThat(userInfo.isAdmin()).isTrue();
@@ -959,6 +991,37 @@
@MediumTest
@Test
+ @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY)
+ public void testRevokeUserAdminThrowsSecurityException() throws Exception {
+ UserInfo targetUser = createUser("SecondaryUser", /*flags=*/ 0);
+ assertThat(targetUser.isAdmin()).isFalse();
+
+ try {
+ // 1. Target User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ targetUser.getUserHandle());
+ assertThrows(SecurityException.class, () -> mUserManager
+ .revokeUserAdmin(targetUser.id));
+
+ // 2. Current User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ mContext.getUser());
+ assertThrows(SecurityException.class, () -> mUserManager
+ .revokeUserAdmin(targetUser.id));
+
+ } finally {
+ // Ensure restriction is removed even if test fails
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ mContext.getUser());
+ }
+ }
+
+ @MediumTest
+ @Test
public void testGetProfileParent() throws Exception {
assumeManagedUsersSupported();
int mainUserId = mUserManager.getMainUser().getIdentifier();
@@ -1184,6 +1247,23 @@
}
}
+ // Make sure createUser for ADMIN would fail if we have DISALLOW_GRANT_ADMIN.
+ @MediumTest
+ @Test
+ @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY)
+ public void testCreateAdminUser_disallowGrantAdmin() throws Exception {
+ final int creatorId = ActivityManager.getCurrentUser();
+ final UserHandle creatorHandle = asHandle(creatorId);
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, creatorHandle);
+ try {
+ UserInfo createdInfo = createUser("SecondaryUser", /*flags=*/ UserInfo.FLAG_ADMIN);
+ assertThat(createdInfo).isNull();
+ } finally {
+ mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, false,
+ creatorHandle);
+ }
+ }
+
// Make sure createProfile would fail if we have DISALLOW_ADD_CLONE_PROFILE.
@MediumTest
@Test
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/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 14ad15e..643ee4a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2458,6 +2458,74 @@
}
@Test
+ public void testBeepVolume_politeNotif_AvalancheStrategy_mixedNotif() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+ initAttentionHelper(flagResolver);
+
+ // Trigger avalanche trigger intent
+ final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", false);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+ // Regular notification: should beep at 0% volume
+ NotificationRecord r = getBeepyNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyBeepVolume(0.0f);
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // Conversation notification
+ mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT);
+ NotificationRecord r2 = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg,
+ "shortcut");
+
+ // Should beep at 100% volume
+ mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+ assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
+ verifyBeepVolume(1.0f);
+
+ // Conversation notification on a different channel
+ mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT);
+ NotificationRecord r3 = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg,
+ "shortcut");
+
+ // Should beep at 50% volume
+ Mockito.reset(mRingtonePlayer);
+ mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+ assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+ verifyBeepVolume(0.5f);
+
+ // 2nd update should beep at 0% volume
+ Mockito.reset(mRingtonePlayer);
+ mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+ verifyBeepVolume(0.0f);
+
+ // Set important conversation
+ mChannel.setImportantConversation(true);
+ r3 = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg,
+ "shortcut");
+
+ // important conversation should beep at 100% volume
+ Mockito.reset(mRingtonePlayer);
+ mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+ verifyBeepVolume(1.0f);
+
+ verify(mAccessibilityService, times(5)).sendAccessibilityEvent(any(), anyInt());
+ assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+ }
+
+ @Test
public void testBeepVolume_politeNotif_Avalanche_exemptEmergency() throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
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/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index 59d5577..1493253 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -21,6 +21,7 @@
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.hardware.vibrator.IVibrator;
import android.os.CombinedVibration;
@@ -32,11 +33,12 @@
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.util.SparseArray;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
import com.android.server.LocalServices;
@@ -76,9 +78,10 @@
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
+ Context context = ApplicationProvider.getApplicationContext();
mTestLooper = new TestLooper();
- mVibrationSettings = new VibrationSettings(
- InstrumentationRegistry.getContext(), new Handler(mTestLooper.getLooper()));
+ mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()),
+ new VibrationConfig(context.getResources()));
SparseArray<VibratorController> vibrators = new SparseArray<>();
vibrators.put(EMPTY_VIBRATOR_ID, createEmptyVibratorController(EMPTY_VIBRATOR_ID));
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
index 9ebeaa8..4704691 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -50,10 +50,9 @@
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import androidx.test.InstrumentationRegistry;
@@ -71,12 +70,13 @@
import org.mockito.junit.MockitoRule;
public class VibrationScalerTest {
+ private static final float TOLERANCE = 1e-2f;
+ private static final int TEST_DEFAULT_AMPLITUDE = 255;
+ private static final float TEST_DEFAULT_SCALE_LEVEL_GAIN = 1.4f;
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
- @Rule
- public final CheckFlagsRule mCheckFlagsRule =
- DeviceFlagsValueProvider.createCheckFlagsRule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock private PowerManagerInternal mPowerManagerInternalMock;
@Mock private PackageManagerInternal mPackageManagerInternalMock;
@@ -96,6 +96,10 @@
when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
when(mPackageManagerInternalMock.getSystemUiServiceComponent())
.thenReturn(new ComponentName("", ""));
+ when(mVibrationConfigMock.getDefaultVibrationAmplitude())
+ .thenReturn(TEST_DEFAULT_AMPLITUDE);
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain())
+ .thenReturn(TEST_DEFAULT_SCALE_LEVEL_GAIN);
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
@@ -107,7 +111,7 @@
mVibrationSettings = new VibrationSettings(
mContextSpy, new Handler(mTestLooper.getLooper()), mVibrationConfigMock);
- mVibrationScaler = new VibrationScaler(mContextSpy, mVibrationSettings);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
mVibrationSettings.onSystemReady();
}
@@ -147,33 +151,76 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
- public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() {
- setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
- setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW);
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
- setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testGetScaleFactor_withLegacyScaling() {
+ // Default scale gain will be ignored.
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()).thenReturn(1.4f);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
+ setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ assertEquals(1.4f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(1.2f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW);
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(0.8f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // LOW
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+ assertEquals(0.6f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_LOW
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+ // Vibration setting being bypassed will use default setting and not scale.
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testGetScaleFactor_withScalingV2() {
+ // Test scale factors for a default gain of 1.4
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()).thenReturn(1.4f);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
+
+ setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ assertEquals(1.95f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(1.4f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW);
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(0.71f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // LOW
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+ assertEquals(0.51f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_LOW
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+ // Vibration setting being bypassed will use default setting and not scale.
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() {
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f);
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f);
assertEquals(0.5f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH));
assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION));
-
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
- // Vibration setting being bypassed will apply adaptive haptics scales.
assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @DisableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void testAdaptiveHapticsScale_flagDisabled_adaptiveHapticScaleAlwaysNone() {
- setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
- setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW);
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
- setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
-
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f);
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f);
@@ -233,7 +280,7 @@
}
@Test
- @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
public void scale_withVendorEffect_setsEffectStrengthBasedOnSettings() {
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_LOW);
setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
@@ -269,13 +316,13 @@
StepSegment resolved = getFirstSegment(mVibrationScaler.scale(
VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE),
USAGE_RINGTONE));
- assertTrue(resolved.getAmplitude() > 0);
+ assertEquals(TEST_DEFAULT_AMPLITUDE / 255f, resolved.getAmplitude(), TOLERANCE);
resolved = getFirstSegment(mVibrationScaler.scale(
VibrationEffect.createWaveform(new long[]{10},
new int[]{VibrationEffect.DEFAULT_AMPLITUDE}, -1),
USAGE_RINGTONE));
- assertTrue(resolved.getAmplitude() > 0);
+ assertEquals(TEST_DEFAULT_AMPLITUDE / 255f, resolved.getAmplitude(), TOLERANCE);
}
@Test
@@ -330,7 +377,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void scale_withAdaptiveHaptics_scalesVibrationsCorrectly() {
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
@@ -351,7 +398,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void scale_clearAdaptiveHapticsScales_clearsAllCachedScales() {
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
@@ -373,7 +420,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void scale_removeAdaptiveHapticsScale_removesCachedScale() {
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
@@ -395,7 +442,7 @@
}
@Test
- @RequiresFlagsEnabled({
+ @EnableFlags({
android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS,
})
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/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 3bd56de..0fbdce4 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -102,6 +102,8 @@
private static final String PACKAGE_NAME = "package";
private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build();
private static final int TEST_RAMP_STEP_DURATION = 5;
+ private static final int TEST_DEFAULT_AMPLITUDE = 255;
+ private static final float TEST_DEFAULT_SCALE_LEVEL_GAIN = 1.4f;
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
@@ -133,6 +135,10 @@
when(mVibrationConfigMock.getDefaultVibrationIntensity(anyInt()))
.thenReturn(Vibrator.VIBRATION_INTENSITY_MEDIUM);
when(mVibrationConfigMock.getRampStepDurationMs()).thenReturn(TEST_RAMP_STEP_DURATION);
+ when(mVibrationConfigMock.getDefaultVibrationAmplitude())
+ .thenReturn(TEST_DEFAULT_AMPLITUDE);
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain())
+ .thenReturn(TEST_DEFAULT_SCALE_LEVEL_GAIN);
when(mPackageManagerInternalMock.getSystemUiServiceComponent())
.thenReturn(new ComponentName("", ""));
doAnswer(answer -> {
@@ -146,7 +152,7 @@
Context context = InstrumentationRegistry.getContext();
mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()),
mVibrationConfigMock);
- mVibrationScaler = new VibrationScaler(context, mVibrationSettings);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
mockVibrators(VIBRATOR_ID);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
index c496bbb..79e272b 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
@@ -37,6 +37,7 @@
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.frameworks.vibrator.ScaleParam;
import android.frameworks.vibrator.VibrationParam;
@@ -46,6 +47,7 @@
import android.os.Process;
import android.os.test.TestLooper;
import android.os.vibrator.Flags;
+import android.os.vibrator.VibrationConfig;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
@@ -97,8 +99,9 @@
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
TestLooper testLooper = new TestLooper();
- mVibrationSettings = new VibrationSettings(
- ApplicationProvider.getApplicationContext(), new Handler(testLooper.getLooper()));
+ Context context = ApplicationProvider.getApplicationContext();
+ mVibrationSettings = new VibrationSettings(context, new Handler(testLooper.getLooper()),
+ new VibrationConfig(context.getResources()));
mFakeVibratorController = new FakeVibratorController(mTestLooper.getLooper());
mVibratorControlService = new VibratorControlService(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index e2524a2..ddadbc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -115,6 +115,17 @@
}
@Test
+ public void testPrimaryDisplayUnchanged_whenWindowingModeAlreadySet_NoFreeformSupport() {
+ mPrimaryDisplay.getDefaultTaskDisplayArea().setWindowingMode(
+ WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
+
+ mDisplayWindowSettings.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW,
+ mPrimaryDisplay.getDefaultTaskDisplayArea().getWindowingMode());
+ }
+
+ @Test
public void testPrimaryDisplayDefaultToFullscreen_HasFreeformSupport_NonPc_NoDesktopMode() {
mWm.mAtmService.mSupportsFreeformWindowManagement = true;
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/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/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index ad6db2d..e657d7f 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -254,7 +254,6 @@
*/
public static final String KEY_PROVISION_SATELLITE_TOKENS = "provision_satellite";
-
/**
* The request was successfully processed.
*/
@@ -2643,7 +2642,7 @@
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
public void requestProvisionSubscriberIds(@NonNull @CallbackExecutor Executor executor,
- @NonNull OutcomeReceiver<List<ProvisionSubscriberId>, SatelliteException> callback) {
+ @NonNull OutcomeReceiver<List<SatelliteSubscriberInfo>, SatelliteException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
@@ -2655,10 +2654,10 @@
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == SATELLITE_RESULT_SUCCESS) {
if (resultData.containsKey(KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN)) {
- List<ProvisionSubscriberId> list =
+ List<SatelliteSubscriberInfo> list =
resultData.getParcelableArrayList(
KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN,
- ProvisionSubscriberId.class);
+ SatelliteSubscriberInfo.class);
executor.execute(() -> Binder.withCleanCallingIdentity(() ->
callback.onResult(list)));
} else {
@@ -2743,9 +2742,9 @@
}
/**
- * Deliver the list of provisioned satellite subscriber ids.
+ * Deliver the list of provisioned satellite subscriber infos.
*
- * @param list List of ProvisionSubscriberId.
+ * @param list The list of provisioned satellite subscriber infos.
* @param executor The executor on which the callback will be called.
* @param callback The callback object to which the result will be delivered.
*
@@ -2754,7 +2753,7 @@
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
- public void provisionSatellite(@NonNull List<ProvisionSubscriberId> list,
+ public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
Objects.requireNonNull(executor);
diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl
similarity index 94%
rename from telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl
rename to telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl
index fe46db8..992c9ae 100644
--- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl
@@ -16,4 +16,4 @@
package android.telephony.satellite;
-parcelable ProvisionSubscriberId;
+parcelable SatelliteSubscriberInfo;
diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
similarity index 83%
rename from telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
rename to telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
index 3e6f743..f26219b 100644
--- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
@@ -26,7 +26,7 @@
import java.util.Objects;
/**
- * ProvisionSubscriberId
+ * SatelliteSubscriberInfo
*
* Satellite Gateway client will use these subscriber ids to register with satellite gateway service
* which identify user subscription with unique subscriber ids. These subscriber ids can be any
@@ -35,7 +35,7 @@
* @hide
*/
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
-public final class ProvisionSubscriberId implements Parcelable {
+public final class SatelliteSubscriberInfo implements Parcelable {
/** provision subscriberId */
@NonNull
private String mSubscriberId;
@@ -49,14 +49,14 @@
/**
* @hide
*/
- public ProvisionSubscriberId(@NonNull String subscriberId, @NonNull int carrierId,
+ public SatelliteSubscriberInfo(@NonNull String subscriberId, @NonNull int carrierId,
@NonNull String niddApn) {
this.mCarrierId = carrierId;
this.mSubscriberId = subscriberId;
this.mNiddApn = niddApn;
}
- private ProvisionSubscriberId(Parcel in) {
+ private SatelliteSubscriberInfo(Parcel in) {
readFromParcel(in);
}
@@ -72,16 +72,16 @@
}
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
- public static final @android.annotation.NonNull Creator<ProvisionSubscriberId> CREATOR =
- new Creator<ProvisionSubscriberId>() {
+ public static final @android.annotation.NonNull Creator<SatelliteSubscriberInfo> CREATOR =
+ new Creator<SatelliteSubscriberInfo>() {
@Override
- public ProvisionSubscriberId createFromParcel(Parcel in) {
- return new ProvisionSubscriberId(in);
+ public SatelliteSubscriberInfo createFromParcel(Parcel in) {
+ return new SatelliteSubscriberInfo(in);
}
@Override
- public ProvisionSubscriberId[] newArray(int size) {
- return new ProvisionSubscriberId[size];
+ public SatelliteSubscriberInfo[] newArray(int size) {
+ return new SatelliteSubscriberInfo[size];
}
};
@@ -148,7 +148,7 @@
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- ProvisionSubscriberId that = (ProvisionSubscriberId) o;
+ SatelliteSubscriberInfo that = (SatelliteSubscriberInfo) o;
return mSubscriberId.equals(that.mSubscriberId) && mCarrierId
== that.mCarrierId && mNiddApn.equals(that.mNiddApn);
}
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl
similarity index 95%
rename from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
rename to telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl
index 460de8c..fb44f87 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl
@@ -19,7 +19,7 @@
/**
* {@hide}
*/
-parcelable ProvisionSubscriberId {
+parcelable SatelliteSubscriberInfo {
/** provision subscriberId */
String subscriberId;
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 2f8e957..8919703 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -78,7 +78,7 @@
import android.telephony.satellite.NtnSignalStrength;
import android.telephony.satellite.SatelliteCapabilities;
import android.telephony.satellite.SatelliteDatagram;
-import android.telephony.satellite.ProvisionSubscriberId;
+import android.telephony.satellite.SatelliteSubscriberInfo;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.telephony.CellNetworkScanResult;
import com.android.internal.telephony.IBooleanConsumer;
@@ -3428,13 +3428,13 @@
void requestIsProvisioned(in String satelliteSubscriberId, in ResultReceiver result);
/**
- * Deliver the list of provisioned satellite subscriber ids.
+ * Deliver the list of provisioned satellite subscriber infos.
*
- * @param list List of provisioned satellite subscriber ids.
+ * @param list The list of provisioned satellite subscriber infos.
* @param result The result receiver that returns whether deliver success or fail.
* @hide
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void provisionSatellite(in List<ProvisionSubscriberId> list, in ResultReceiver result);
+ void provisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
}
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 827ff4f..ad98e47 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -24,6 +24,7 @@
"flickerlib-parsers",
"perfetto_trace_java_protos",
"flickerlib-trace_processor_shell",
+ "ravenwood-junit",
],
java_resource_dirs: ["res"],
certificate: "platform",
@@ -39,6 +40,7 @@
"platform-test-annotations",
],
srcs: [
+ "src/com/android/internal/graphics/ColorUtilsTest.java",
"src/com/android/internal/util/ParcellingTests.java",
],
auto_gen_config: true,
diff --git a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java
index d0bb8e3..38a22f2 100644
--- a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java
+++ b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java
@@ -19,14 +19,19 @@
import static org.junit.Assert.assertTrue;
import android.graphics.Color;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
+import org.junit.Rule;
import org.junit.Test;
@SmallTest
public class ColorUtilsTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
@Test
public void calculateMinimumBackgroundAlpha_satisfiestContrast() {
diff --git a/tests/Internal/src/com/android/internal/util/ParcellingTests.java b/tests/Internal/src/com/android/internal/util/ParcellingTests.java
index 65a3436..fb63422 100644
--- a/tests/Internal/src/com/android/internal/util/ParcellingTests.java
+++ b/tests/Internal/src/com/android/internal/util/ParcellingTests.java
@@ -18,6 +18,7 @@
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -26,6 +27,7 @@
import com.android.internal.util.Parcelling.BuiltIn.ForInstant;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -38,6 +40,9 @@
@RunWith(JUnit4.class)
public class ParcellingTests {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
private Parcel mParcel = Parcel.obtain();
@Test
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 3f9016b..f43cf52 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -113,6 +113,7 @@
"io/ZipArchive.cpp",
"link/AutoVersioner.cpp",
"link/FeatureFlagsFilter.cpp",
+ "link/FlagDisabledResourceRemover.cpp",
"link/ManifestFixer.cpp",
"link/NoDefaultResourceRemover.cpp",
"link/PrivateAttributeMover.cpp",
@@ -189,6 +190,8 @@
"integration-tests/CommandTests/**/*",
"integration-tests/ConvertTest/**/*",
"integration-tests/DumpTest/**/*",
+ ":resource-flagging-test-app-apk",
+ ":resource-flagging-test-app-r-java",
],
}
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 9444dd9..1c85e9f 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -690,9 +690,7 @@
resource_format = item_iter->second.format;
}
- // Don't bother parsing the item if it is behind a disabled flag
- if (out_resource->flag_status != FlagStatus::Disabled &&
- !ParseItem(parser, out_resource, resource_format)) {
+ if (!ParseItem(parser, out_resource, resource_format)) {
return false;
}
return true;
diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp
index 2e6ad13..b59b165 100644
--- a/tools/aapt2/ResourceParser_test.cpp
+++ b/tools/aapt2/ResourceParser_test.cpp
@@ -69,13 +69,8 @@
return TestParse(str, ConfigDescription{});
}
- ::testing::AssertionResult TestParse(StringPiece str, ResourceParserOptions parserOptions) {
- return TestParse(str, ConfigDescription{}, parserOptions);
- }
-
- ::testing::AssertionResult TestParse(
- StringPiece str, const ConfigDescription& config,
- ResourceParserOptions parserOptions = ResourceParserOptions()) {
+ ::testing::AssertionResult TestParse(StringPiece str, const ConfigDescription& config) {
+ ResourceParserOptions parserOptions;
ResourceParser parser(context_->GetDiagnostics(), &table_, android::Source{"test"}, config,
parserOptions);
@@ -247,19 +242,6 @@
EXPECT_FALSE(TestParse(R"(<string name="foo4" translatable="yes">Translate</string>)"));
}
-TEST_F(ResourceParserTest, ParseStringBehindDisabledFlag) {
- FeatureFlagProperties flag_properties(true, false);
- ResourceParserOptions options;
- options.feature_flag_values = {{"falseFlag", flag_properties}};
- ASSERT_TRUE(TestParse(
- R"(<string name="foo" android:featureFlag="falseFlag"
- xmlns:android="http://schemas.android.com/apk/res/android">foo</string>)",
- options));
-
- String* str = test::GetValue<String>(&table_, "string/foo");
- ASSERT_THAT(str, IsNull());
-}
-
TEST_F(ResourceParserTest, IgnoreXliffTagsOtherThanG) {
std::string input = R"(
<string name="foo" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
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/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 642a561..56f5288 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -57,6 +57,7 @@
#include "java/ManifestClassGenerator.h"
#include "java/ProguardRules.h"
#include "link/FeatureFlagsFilter.h"
+#include "link/FlagDisabledResourceRemover.h"
#include "link/Linkers.h"
#include "link/ManifestFixer.h"
#include "link/NoDefaultResourceRemover.h"
@@ -1840,11 +1841,57 @@
return validate(attr->value);
}
+ class FlagDisabledStringVisitor : public DescendingValueVisitor {
+ public:
+ using DescendingValueVisitor::Visit;
+
+ explicit FlagDisabledStringVisitor(android::StringPool& string_pool)
+ : string_pool_(string_pool) {
+ }
+
+ void Visit(RawString* value) override {
+ value->value = string_pool_.MakeRef("");
+ }
+
+ void Visit(String* value) override {
+ value->value = string_pool_.MakeRef("");
+ }
+
+ void Visit(StyledString* value) override {
+ value->value = string_pool_.MakeRef(android::StyleString{{""}, {}});
+ }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FlagDisabledStringVisitor);
+ android::StringPool& string_pool_;
+ };
+
// Writes the AndroidManifest, ResourceTable, and all XML files referenced by the ResourceTable
// to the IArchiveWriter.
bool WriteApk(IArchiveWriter* writer, proguard::KeepSet* keep_set, xml::XmlResource* manifest,
ResourceTable* table) {
TRACE_CALL();
+
+ FlagDisabledStringVisitor visitor(table->string_pool);
+
+ for (auto& package : table->packages) {
+ for (auto& type : package->types) {
+ for (auto& entry : type->entries) {
+ for (auto& config_value : entry->values) {
+ if (config_value->flag_status == FlagStatus::Disabled) {
+ config_value->value->Accept(&visitor);
+ }
+ }
+ }
+ }
+ }
+
+ if (!FlagDisabledResourceRemover{}.Consume(context_, table)) {
+ context_->GetDiagnostics()->Error(android::DiagMessage()
+ << "failed removing resources behind disabled flags");
+ return 1;
+ }
+
const bool keep_raw_values = (context_->GetPackageType() == PackageType::kStaticLib)
|| options_.keep_raw_values;
bool result = FlattenXml(context_, *manifest, kAndroidManifestPath, keep_raw_values,
@@ -2331,6 +2378,12 @@
return 1;
};
+ if (options_.generate_java_class_path || options_.generate_text_symbols_path) {
+ if (!GenerateJavaClasses()) {
+ return 1;
+ }
+ }
+
if (!WriteApk(archive_writer.get(), &proguard_keep_set, manifest_xml.get(), &final_table_)) {
return 1;
}
@@ -2339,12 +2392,6 @@
return 1;
}
- if (options_.generate_java_class_path || options_.generate_text_symbols_path) {
- if (!GenerateJavaClasses()) {
- return 1;
- }
- }
-
if (!WriteProguardFile(options_.generate_proguard_rules_path, proguard_keep_set)) {
return 1;
}
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
new file mode 100644
index 0000000..5932271
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
@@ -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 {
+ // 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"],
+ default_team: "trendy_team_android_resources",
+}
+
+genrule {
+ name: "resource-flagging-test-app-compile",
+ tools: ["aapt2"],
+ srcs: [
+ "res/values/bools.xml",
+ "res/values/bools2.xml",
+ "res/values/strings.xml",
+ ],
+ out: [
+ "values_bools.arsc.flat",
+ "values_bools2.arsc.flat",
+ "values_strings.arsc.flat",
+ ],
+ cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
+ "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
+}
+
+genrule {
+ name: "resource-flagging-test-app-apk",
+ tools: ["aapt2"],
+ // The first input file in the list must be the manifest
+ srcs: [
+ "AndroidManifest.xml",
+ ":resource-flagging-test-app-compile",
+ ],
+ out: [
+ "resapp.apk",
+ ],
+ cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
+}
+
+genrule {
+ name: "resource-flagging-test-app-r-java",
+ tools: ["aapt2"],
+ // The first input file in the list must be the manifest
+ srcs: [
+ "AndroidManifest.xml",
+ ":resource-flagging-test-app-compile",
+ ],
+ out: [
+ "resource-flagging-java/com/android/intenal/flaggedresources/R.java",
+ ],
+ cmd: "$(location aapt2) link -o $(genDir)/resapp.apk --java $(genDir)/resource-flagging-java --manifest $(in)",
+}
+
+java_genrule {
+ name: "resource-flagging-test-app-apk-as-resource",
+ srcs: [
+ ":resource-flagging-test-app-apk",
+ ],
+ out: ["apks_as_resources.res.zip"],
+ tools: ["soong_zip"],
+
+ cmd: "mkdir -p $(genDir)/res/raw && " +
+ "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " +
+ "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
+}
diff --git a/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/AndroidManifest.xml
similarity index 100%
rename from core/tests/resourceflaggingtests/TestAppAndroidManifest.xml
rename to tools/aapt2/integration-tests/FlaggedResourcesTest/AndroidManifest.xml
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
similarity index 82%
rename from core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
rename to tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
index 8d01465..3e094fb 100644
--- a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
@@ -7,4 +7,6 @@
<bool name="res2" android:featureFlag="test.package.trueFlag">true</bool>
<bool name="res3">false</bool>
+
+ <bool name="res4" android:featureFlag="test.package.falseFlag">true</bool>
</resources>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
similarity index 100%
rename from core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml
rename to tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml
new file mode 100644
index 0000000..5c0fca1
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <string name="str">plain string</string>
+
+ <string name="str1" android:featureFlag="test.package.falseFlag">DONTFIND</string>
+</resources>
\ No newline at end of file
diff --git a/tools/aapt2/link/FlagDisabledResourceRemover.cpp b/tools/aapt2/link/FlagDisabledResourceRemover.cpp
new file mode 100644
index 0000000..e3289e2
--- /dev/null
+++ b/tools/aapt2/link/FlagDisabledResourceRemover.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+#include "link/FlagDisabledResourceRemover.h"
+
+#include <algorithm>
+
+#include "ResourceTable.h"
+
+using android::ConfigDescription;
+
+namespace aapt {
+
+static bool KeepResourceEntry(const std::unique_ptr<ResourceEntry>& entry) {
+ if (entry->values.empty()) {
+ return true;
+ }
+ const auto end_iter = entry->values.end();
+ const auto remove_iter =
+ std::stable_partition(entry->values.begin(), end_iter,
+ [](const std::unique_ptr<ResourceConfigValue>& value) -> bool {
+ return value->flag_status != FlagStatus::Disabled;
+ });
+
+ bool keep = remove_iter != entry->values.begin();
+
+ entry->values.erase(remove_iter, end_iter);
+ return keep;
+}
+
+bool FlagDisabledResourceRemover::Consume(IAaptContext* context, ResourceTable* table) {
+ for (auto& pkg : table->packages) {
+ for (auto& type : pkg->types) {
+ const auto end_iter = type->entries.end();
+ const auto remove_iter = std::stable_partition(
+ type->entries.begin(), end_iter, [](const std::unique_ptr<ResourceEntry>& entry) -> bool {
+ return KeepResourceEntry(entry);
+ });
+
+ type->entries.erase(remove_iter, end_iter);
+ }
+ }
+ return true;
+}
+
+} // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/link/FlagDisabledResourceRemover.h b/tools/aapt2/link/FlagDisabledResourceRemover.h
new file mode 100644
index 0000000..2db2cb4
--- /dev/null
+++ b/tools/aapt2/link/FlagDisabledResourceRemover.h
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "android-base/macros.h"
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+// Removes any resource that are behind disabled flags.
+class FlagDisabledResourceRemover : public IResourceTableConsumer {
+ public:
+ FlagDisabledResourceRemover() = default;
+
+ bool Consume(IAaptContext* context, ResourceTable* table) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FlagDisabledResourceRemover);
+};
+
+} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
new file mode 100644
index 0000000..c901b58
--- /dev/null
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LoadedApk.h"
+#include "cmd/Dump.h"
+#include "io/StringStream.h"
+#include "test/Test.h"
+#include "text/Printer.h"
+
+using ::aapt::io::StringOutputStream;
+using ::aapt::text::Printer;
+using testing::Eq;
+using testing::Ne;
+
+namespace aapt {
+
+using FlaggedResourcesTest = CommandTestFixture;
+
+static android::NoOpDiagnostics noop_diag;
+
+void DumpStringPoolToString(LoadedApk* loaded_apk, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ DumpStringsCommand command(&printer, &noop_diag);
+ ASSERT_EQ(command.Dump(loaded_apk), 0);
+ output_stream.Flush();
+}
+
+void DumpResourceTableToString(LoadedApk* loaded_apk, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ DumpTableCommand command(&printer, &noop_diag);
+ ASSERT_EQ(command.Dump(loaded_apk), 0);
+ output_stream.Flush();
+}
+
+void DumpChunksToString(LoadedApk* loaded_apk, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ DumpChunks command(&printer, &noop_diag);
+ ASSERT_EQ(command.Dump(loaded_apk), 0);
+ output_stream.Flush();
+}
+
+TEST_F(FlaggedResourcesTest, DisabledStringRemovedFromPool) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpStringPoolToString(loaded_apk.get(), &output);
+
+ std::string excluded = "DONTFIND";
+ ASSERT_EQ(output.find(excluded), std::string::npos);
+}
+
+TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTable) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpResourceTableToString(loaded_apk.get(), &output);
+}
+
+TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpChunksToString(loaded_apk.get(), &output);
+
+ ASSERT_EQ(output.find("res4"), std::string::npos);
+ ASSERT_EQ(output.find("str1"), std::string::npos);
+}
+
+TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) {
+ auto r_path = file::BuildPath({android::base::GetExecutableDirectory(), "resource-flagging-java",
+ "com", "android", "intenal", "flaggedresources", "R.java"});
+ std::string r_contents;
+ ::android::base::ReadFileToString(r_path, &r_contents);
+
+ ASSERT_NE(r_contents.find("public static final int res4"), std::string::npos);
+ ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos);
+}
+
+} // namespace aapt
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 5dde265..36bfbef 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -58,17 +58,19 @@
// Dump the classes, if specified.
options.inputJarDumpFile.ifSet {
- PrintWriter(it).use { pw -> allClasses.dump(pw) }
- log.i("Dump file created at $it")
+ log.iTime("Dump file created at $it") {
+ PrintWriter(it).use { pw -> allClasses.dump(pw) }
+ }
}
options.inputJarAsKeepAllFile.ifSet {
- PrintWriter(it).use {
- pw -> allClasses.forEach {
- classNode -> printAsTextPolicy(pw, classNode)
+ log.iTime("Dump file created at $it") {
+ PrintWriter(it).use { pw ->
+ allClasses.forEach { classNode ->
+ printAsTextPolicy(pw, classNode)
+ }
}
}
- log.i("Dump file created at $it")
}
// Build the filters.
@@ -91,16 +93,18 @@
// Dump statistics, if specified.
options.statsFile.ifSet {
- PrintWriter(it).use { pw -> stats.dumpOverview(pw) }
- log.i("Dump file created at $it")
+ log.iTime("Dump file created at $it") {
+ PrintWriter(it).use { pw -> stats.dumpOverview(pw) }
+ }
}
options.apiListFile.ifSet {
- PrintWriter(it).use { pw ->
- // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed
- // framework-minus-apex.jar so that we can dump inherited methods from it.
- ApiDumper(pw, allClasses, null, filter).dump()
+ log.iTime("API list file created at $it") {
+ PrintWriter(it).use { pw ->
+ // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed
+ // framework-minus-apex.jar so that we can dump inherited methods from it.
+ ApiDumper(pw, allClasses, null, filter).dump()
+ }
}
- log.i("API list file created at $it")
}
}
@@ -221,47 +225,48 @@
log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar)
log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled")
- val start = System.currentTimeMillis()
+ log.iTime("Transforming jar") {
+ val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
- val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+ var itemIndex = 0
+ var numItemsProcessed = 0
+ var numItems = -1 // == Unknown
- var itemIndex = 0
- var numItemsProcessed = 0
- var numItems = -1 // == Unknown
+ log.withIndent {
+ // Open the input jar file and process each entry.
+ ZipFile(inJar).use { inZip ->
- log.withIndent {
- // Open the input jar file and process each entry.
- ZipFile(inJar).use { inZip ->
+ numItems = inZip.size()
+ val shardStart = numItems * shard / numShards
+ val shardNextStart = numItems * (shard + 1) / numShards
- numItems = inZip.size()
- val shardStart = numItems * shard / numShards
- val shardNextStart = numItems * (shard + 1) / numShards
-
- maybeWithZipOutputStream(outStubJar) { stubOutStream ->
- maybeWithZipOutputStream(outImplJar) { implOutStream ->
- val inEntries = inZip.entries()
- while (inEntries.hasMoreElements()) {
- val entry = inEntries.nextElement()
- val inShard = (shardStart <= itemIndex) && (itemIndex < shardNextStart)
- itemIndex++
- if (!inShard) {
- continue
- }
- convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
+ maybeWithZipOutputStream(outStubJar) { stubOutStream ->
+ maybeWithZipOutputStream(outImplJar) { implOutStream ->
+ val inEntries = inZip.entries()
+ while (inEntries.hasMoreElements()) {
+ val entry = inEntries.nextElement()
+ val inShard = (shardStart <= itemIndex)
+ && (itemIndex < shardNextStart)
+ itemIndex++
+ if (!inShard) {
+ continue
+ }
+ convertSingleEntry(
+ inZip, entry, stubOutStream, implOutStream,
filter, packageRedirector, remapper,
- enableChecker, classes, errors, stats)
- numItemsProcessed++
+ enableChecker, classes, errors, stats
+ )
+ numItemsProcessed++
+ }
+ log.i("Converted all entries.")
}
- log.i("Converted all entries.")
}
+ outStubJar?.let { log.i("Created stub: $it") }
+ outImplJar?.let { log.i("Created impl: $it") }
}
- outStubJar?.let { log.i("Created stub: $it") }
- outImplJar?.let { log.i("Created impl: $it") }
}
+ log.i("%d / %d item(s) processed.", numItemsProcessed, numItems)
}
- val end = System.currentTimeMillis()
- log.i("Done transforming the jar in %.1f second(s). %d / %d item(s) processed.",
- (end - start) / 1000.0, numItemsProcessed, numItems)
}
private fun <T> maybeWithZipOutputStream(filename: String?, block: (ZipOutputStream?) -> T): T {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
index 18065ba..ee4a06f 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
@@ -185,6 +185,16 @@
println(LogLevel.Debug, format, *args)
}
+ inline fun <T> iTime(message: String, block: () -> T): T {
+ val start = System.currentTimeMillis()
+ val ret = block()
+ val end = System.currentTimeMillis()
+
+ log.i("%s: took %.1f second(s).", message, (end - start) / 1000.0)
+
+ return ret
+ }
+
inline fun forVerbose(block: () -> Unit) {
if (isEnabled(LogLevel.Verbose)) {
block()
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
index 92906a7..2607df6 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
@@ -184,49 +184,50 @@
* Load all the classes, without code.
*/
fun loadClassStructures(inJar: String): ClassNodes {
- log.i("Reading class structure from $inJar ...")
- val start = System.currentTimeMillis()
+ log.iTime("Reading class structure from $inJar") {
+ val allClasses = ClassNodes()
- val allClasses = ClassNodes()
+ log.withIndent {
+ ZipFile(inJar).use { inZip ->
+ val inEntries = inZip.entries()
- log.withIndent {
- ZipFile(inJar).use { inZip ->
- val inEntries = inZip.entries()
+ while (inEntries.hasMoreElements()) {
+ val entry = inEntries.nextElement()
- while (inEntries.hasMoreElements()) {
- val entry = inEntries.nextElement()
-
- BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
- if (entry.name.endsWith(".class")) {
- val cr = ClassReader(bis)
- val cn = ClassNode()
- cr.accept(cn, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG
- or ClassReader.SKIP_FRAMES)
- if (!allClasses.addClass(cn)) {
- log.w("Duplicate class found: ${cn.name}")
- }
- } else if (entry.name.endsWith(".dex")) {
- // Seems like it's an ART jar file. We can't process it.
- // It's a fatal error.
- throw InvalidJarFileException(
- "$inJar is not a desktop jar file. It contains a *.dex file.")
- } else {
- // Unknown file type. Skip.
- while (bis.available() > 0) {
- bis.skip((1024 * 1024).toLong())
+ BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
+ if (entry.name.endsWith(".class")) {
+ val cr = ClassReader(bis)
+ val cn = ClassNode()
+ cr.accept(
+ cn, ClassReader.SKIP_CODE
+ or ClassReader.SKIP_DEBUG
+ or ClassReader.SKIP_FRAMES
+ )
+ if (!allClasses.addClass(cn)) {
+ log.w("Duplicate class found: ${cn.name}")
+ }
+ } else if (entry.name.endsWith(".dex")) {
+ // Seems like it's an ART jar file. We can't process it.
+ // It's a fatal error.
+ throw InvalidJarFileException(
+ "$inJar is not a desktop jar file."
+ + " It contains a *.dex file."
+ )
+ } else {
+ // Unknown file type. Skip.
+ while (bis.available() > 0) {
+ bis.skip((1024 * 1024).toLong())
+ }
}
}
}
}
}
+ if (allClasses.size == 0) {
+ log.w("$inJar contains no *.class files.")
+ }
+ return allClasses
}
- if (allClasses.size == 0) {
- log.w("$inJar contains no *.class files.")
- }
-
- val end = System.currentTimeMillis()
- log.i("Done reading class structure in %.1f second(s).", (end - start) / 1000.0)
- return allClasses
}
}
}
\ No newline at end of file