Merge "Fix typo TOLERENCE --> TOLERANCE" am: aefe92f478 am: 9a1634a944
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/2407716
Change-Id: I559930d33986ffe298c6d549f86b2f739418ccea
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index bb52cfb..3fe8d03 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1140,7 +1140,6 @@
public final class CameraManager {
method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException;
method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
- field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L
}
public abstract static class CameraManager.AvailabilityCallback {
@@ -2425,6 +2424,7 @@
public abstract class DreamOverlayService extends android.app.Service {
ctor public DreamOverlayService();
method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
+ method public void onEndDream();
method public abstract void onStartDream(@NonNull android.view.WindowManager.LayoutParams);
method public final void requestExit();
method public final boolean shouldShowComplications();
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index d24b677..03a97ba 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -997,6 +997,8 @@
private ComponentCallbacksController mCallbacksController;
+ @Nullable private IVoiceInteractionManagerService mVoiceInteractionManagerService;
+
private final WindowControllerCallback mWindowControllerCallback =
new WindowControllerCallback() {
/**
@@ -1606,18 +1608,17 @@
private void notifyVoiceInteractionManagerServiceActivityEvent(
@VoiceInteractionSession.VoiceInteractionActivityEventType int type) {
-
- final IVoiceInteractionManagerService service =
- IVoiceInteractionManagerService.Stub.asInterface(
- ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
- if (service == null) {
- Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
- + "VoiceInteractionManagerService");
- return;
+ if (mVoiceInteractionManagerService == null) {
+ mVoiceInteractionManagerService = IVoiceInteractionManagerService.Stub.asInterface(
+ ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+ if (mVoiceInteractionManagerService == null) {
+ Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
+ + "VoiceInteractionManagerService");
+ return;
+ }
}
-
try {
- service.notifyActivityEventChanged(mToken, type);
+ mVoiceInteractionManagerService.notifyActivityEventChanged(mToken, type);
} catch (RemoteException e) {
// Empty
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 7f4af8b..834d186 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -7948,8 +7948,6 @@
* @hide
*/
public MessagingStyle setShortcutIcon(@Nullable Icon conversationIcon) {
- // TODO(b/228941516): This icon should be downscaled to avoid using too much memory,
- // see reduceImageSizes.
mShortcutIcon = conversationIcon;
return this;
}
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index a51b9d3..27f9f54 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1406,6 +1406,17 @@
}
/**
+ * Return the number of entries in the cache. This is used for testing and has package-only
+ * visibility.
+ * @hide
+ */
+ public int size() {
+ synchronized (mLock) {
+ return mCache.size();
+ }
+ }
+
+ /**
* Returns a list of caches alive at the current time.
*/
@GuardedBy("sGlobalLock")
@@ -1612,8 +1623,12 @@
* @hide
*/
public static void onTrimMemory() {
- for (PropertyInvalidatedCache pic : getActiveCaches()) {
- pic.clear();
+ ArrayList<PropertyInvalidatedCache> activeCaches;
+ synchronized (sGlobalLock) {
+ activeCaches = getActiveCaches();
+ }
+ for (int i = 0; i < activeCaches.size(); i++) {
+ activeCaches.get(i).clear();
}
}
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 67d5148..f360121 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6789,6 +6789,8 @@
* {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE},
* {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY},
* {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}.
+ *
+ * @throws SecurityException if called on a parent instance.
*/
public int getStorageEncryptionStatus() {
throwIfParentInstance("getStorageEncryptionStatus");
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 2ea0d82..3daee1f 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5737,7 +5737,7 @@
* @hide
*/
public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS =
- "android.intent.extra.EXTRA_CHOOSER_CUSTOM_ACTIONS";
+ "android.intent.extra.CHOOSER_CUSTOM_ACTIONS";
/**
* Optional argument to be used with {@link #ACTION_CHOOSER}.
@@ -5748,7 +5748,7 @@
* @hide
*/
public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION =
- "android.intent.extra.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION";
+ "android.intent.extra.CHOOSER_PAYLOAD_RESELECTION_ACTION";
/**
* An {@code ArrayList} of {@code String} annotations describing content for
@@ -11488,7 +11488,7 @@
private void toUriInner(StringBuilder uri, String scheme, String defAction,
String defPackage, int flags) {
if (scheme != null) {
- uri.append("scheme=").append(scheme).append(';');
+ uri.append("scheme=").append(Uri.encode(scheme)).append(';');
}
if (mAction != null && !mAction.equals(defAction)) {
uri.append("action=").append(Uri.encode(mAction)).append(';');
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 9e5e8de..80cea55 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1058,6 +1058,41 @@
public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id
/**
+ * This change id excludes the packages it is applied to from the camera compat force rotation
+ * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context.
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION =
+ 263959004L; // buganizer id
+
+ /**
+ * This change id excludes the packages it is applied to from activity refresh after camera
+ * compat force rotation treatment. See com.android.server.wm.DisplayRotationCompatPolicy for
+ * context.
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id
+
+ /**
+ * This change id makes the packages it is applied to do activity refresh after camera compat
+ * force rotation treatment using "resumed -> paused -> resumed" cycle rather than "resumed ->
+ * ... -> stopped -> ... -> resumed" cycle. See
+ * com.android.server.wm.DisplayRotationCompatPolicy for context.
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE =
+ 264301586L; // buganizer id
+
+ /**
* This change id is the gatekeeper for all treatments that force a given min aspect ratio.
* Enabling this change will allow the following min aspect ratio treatments to be applied:
* OVERRIDE_MIN_ASPECT_RATIO_MEDIUM
@@ -1152,6 +1187,79 @@
@Overridable
public static final long OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS = 263259275L;
+ // Compat framework that per-app overrides rely on only supports booleans. That's why we have
+ // multiple OVERRIDE_*_ORIENTATION_* change ids below instead of just one override with
+ // the integer value.
+
+ /**
+ * Enables {@link #SCREEN_ORIENTATION_PORTRAIT}. Unless OVERRIDE_ANY_ORIENTATION
+ * is enabled, this override is used only when no other fixed orientation was specified by the
+ * activity.
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT = 265452344L;
+
+ /**
+ * Enables {@link #SCREEN_ORIENTATION_NOSENSOR}. Unless OVERRIDE_ANY_ORIENTATION
+ * is enabled, this override is used only when no other fixed orientation was specified by the
+ * activity.
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR = 265451093L;
+
+ /**
+ * Enables {@link #SCREEN_ORIENTATION_REVERSE_LANDSCAPE}. Unless OVERRIDE_ANY_ORIENTATION
+ * is enabled, this override is used only when activity specify landscape orientation.
+ * This can help apps that assume that landscape display orientation corresponds to {@link
+ * android.view.Surface#ROTATION_90}, while on some devices it can be {@link
+ * android.view.Surface#ROTATION_270}.
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE = 266124927L;
+
+ /**
+ * When enabled, allows OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE,
+ * OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR and OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+ * to override any orientation requested by the activity.
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_ANY_ORIENTATION = 265464455L;
+
+ /**
+ * This override fixes display orientation to landscape natural orientation when a task is
+ * fullscreen. While display rotation is fixed to landscape, the orientation requested by the
+ * activity will be still respected by bounds resolution logic. For instance, if an activity
+ * requests portrait orientation and this override is set, then activity will appear in the
+ * letterbox mode for fixed orientation with the display rotated to the lanscape natural
+ * orientation.
+ *
+ * <p>This override is applicable only when natural orientation of the device is
+ * landscape and display ignores orientation requestes.
+ *
+ * <p>Main use case for this override are camera-using activities that are portrait-only and
+ * assume alignment with natural device orientation. Such activities can automatically be
+ * rotated with com.android.server.wm.DisplayRotationCompatPolicy but not all of them can
+ * handle dynamic rotation and thus can benefit from this override.
+ *
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION = 255940284L;
+
/**
* Compares activity window layout min width/height with require space for multi window to
* determine if it can be put into multi window mode.
@@ -1370,8 +1478,19 @@
* @hide
*/
public boolean isFixedOrientation() {
- return isFixedOrientationLandscape() || isFixedOrientationPortrait()
- || screenOrientation == SCREEN_ORIENTATION_LOCKED;
+ return isFixedOrientation(screenOrientation);
+ }
+
+ /**
+ * Returns true if the passed activity's orientation is fixed.
+ * @hide
+ */
+ public static boolean isFixedOrientation(@ScreenOrientation int orientation) {
+ return orientation == SCREEN_ORIENTATION_LOCKED
+ // Orientation is fixed to natural display orientation
+ || orientation == SCREEN_ORIENTATION_NOSENSOR
+ || isFixedOrientationLandscape(orientation)
+ || isFixedOrientationPortrait(orientation);
}
/**
diff --git a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
index 41f2f9c..b86b97c 100644
--- a/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
+++ b/core/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
@@ -17,7 +17,7 @@
package android.database.sqlite;
/**
- * Thrown if the the bind or column parameter index is out of range
+ * Thrown if the bind or column parameter index is out of range.
*/
public class SQLiteBindOrColumnIndexOutOfRangeException extends SQLiteException {
public SQLiteBindOrColumnIndexOutOfRangeException() {}
diff --git a/core/java/android/database/sqlite/SQLiteDiskIOException.java b/core/java/android/database/sqlite/SQLiteDiskIOException.java
index 01b2069..152d90a 100644
--- a/core/java/android/database/sqlite/SQLiteDiskIOException.java
+++ b/core/java/android/database/sqlite/SQLiteDiskIOException.java
@@ -17,7 +17,7 @@
package android.database.sqlite;
/**
- * An exception that indicates that an IO error occured while accessing the
+ * Indicates that an IO error occurred while accessing the
* SQLite database file.
*/
public class SQLiteDiskIOException extends SQLiteException {
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 5291d2b..7ccf07a 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -29,7 +29,6 @@
import android.annotation.SdkConstant.SdkConstantType;
import android.app.ActivityThread;
import android.app.AppOpsManager;
-import android.app.compat.CompatChanges;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.graphics.ImageFormat;
@@ -47,7 +46,6 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemProperties;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RSIllegalArgumentException;
@@ -284,14 +282,6 @@
*/
public native static int getNumberOfCameras();
- private static final boolean sLandscapeToPortrait =
- SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false);
-
- private static boolean shouldOverrideToPortrait() {
- return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT)
- && sLandscapeToPortrait;
- }
-
/**
* Returns the information about a particular camera.
* If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1.
@@ -301,7 +291,8 @@
* low-level failure).
*/
public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) {
- boolean overrideToPortrait = shouldOverrideToPortrait();
+ boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(
+ ActivityThread.currentApplication().getApplicationContext());
_getCameraInfo(cameraId, overrideToPortrait, cameraInfo);
IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
@@ -498,7 +489,8 @@
mEventHandler = null;
}
- boolean overrideToPortrait = shouldOverrideToPortrait();
+ boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(
+ ActivityThread.currentApplication().getApplicationContext());
return native_setup(new WeakReference<Camera>(this), cameraId,
ActivityThread.currentOpPackageName(), overrideToPortrait);
}
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 10a7538..2f81e0c 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -880,8 +880,8 @@
* <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code VIDEO_CALL}</td> <td colspan="3" id="rb"></td> <td>Preview with video call</td> </tr>
* <tr> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code PREVIEW_VIDEO_STILL}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>Multi-purpose stream with JPEG or YUV still capture</td> </tr>
* <tr> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>{@code JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>YUV and JPEG concurrent still image capture (for testing)</td> </tr>
- * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, video record and JPEG or YUV video snapshot</td> </tr>
- * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, in-application image processing, and JPEG or YUV still image capture</td> </tr>
+ * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td>{@code JPEG}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, video record and JPEG video snapshot</td> </tr>
+ * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, in-application image processing, and JPEG still image capture</td> </tr>
* </table><br>
* </p>
*
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index ed1e9e5..30c4101 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -115,8 +115,14 @@
@ChangeId
@Overridable
@EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
- @TestApi
- public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L;
+ public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L;
+
+ /**
+ * Package-level opt in/out for the above.
+ * @hide
+ */
+ public static final String PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT =
+ "android.camera.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT";
/**
* System property for allowing the above
@@ -392,6 +398,23 @@
* except that it uses {@link java.util.concurrent.Executor} as an argument
* instead of {@link android.os.Handler}.</p>
*
+ * <p>Note: If the order between some availability callbacks matters, the implementation of the
+ * executor should handle those callbacks in the same thread to maintain the callbacks' order.
+ * Some examples are:</p>
+ *
+ * <ul>
+ *
+ * <li>{@link AvailabilityCallback#onCameraAvailable} and
+ * {@link AvailabilityCallback#onCameraUnavailable} of the same camera ID.</li>
+ *
+ * <li>{@link AvailabilityCallback#onCameraAvailable} or
+ * {@link AvailabilityCallback#onCameraUnavailable} of a logical multi-camera, and {@link
+ * AvailabilityCallback#onPhysicalCameraUnavailable} or
+ * {@link AvailabilityCallback#onPhysicalCameraAvailable} of its physical
+ * cameras.</li>
+ *
+ * </ul>
+ *
* @param executor The executor which will be used to invoke the callback.
* @param callback the new callback to send camera availability notices to
*
@@ -608,7 +631,7 @@
try {
Size displaySize = getDisplaySize();
- boolean overrideToPortrait = shouldOverrideToPortrait();
+ boolean overrideToPortrait = shouldOverrideToPortrait(mContext);
CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId,
mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait);
try {
@@ -728,7 +751,7 @@
"Camera service is currently unavailable");
}
- boolean overrideToPortrait = shouldOverrideToPortrait();
+ boolean overrideToPortrait = shouldOverrideToPortrait(mContext);
cameraUser = cameraService.connectDevice(callbacks, cameraId,
mContext.getOpPackageName(), mContext.getAttributionTag(), uid,
oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion,
@@ -1160,9 +1183,26 @@
return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId);
}
- private static boolean shouldOverrideToPortrait() {
- return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT)
- && CameraManagerGlobal.sLandscapeToPortrait;
+ /**
+ * @hide
+ */
+ public static boolean shouldOverrideToPortrait(@Nullable Context context) {
+ if (!CameraManagerGlobal.sLandscapeToPortrait) {
+ return false;
+ }
+
+ if (context != null) {
+ PackageManager packageManager = context.getPackageManager();
+
+ try {
+ return packageManager.getProperty(context.getOpPackageName(),
+ PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT).getBoolean();
+ } catch (PackageManager.NameNotFoundException e) {
+ // No such property
+ }
+ }
+
+ return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT);
}
/**
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index a6c79b3..0c2468e 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -87,6 +87,7 @@
// TODO: guard every function with if (!mRemoteDevice) check (if it was closed)
private ICameraDeviceUserWrapper mRemoteDevice;
+ private boolean mRemoteDeviceInit = false;
// Lock to synchronize cross-thread access to device public interface
final Object mInterfaceLock = new Object(); // access from this class and Session only!
@@ -338,6 +339,8 @@
mDeviceExecutor.execute(mCallOnOpened);
mDeviceExecutor.execute(mCallOnUnconfigured);
+
+ mRemoteDeviceInit = true;
}
}
@@ -1754,8 +1757,8 @@
}
synchronized(mInterfaceLock) {
- if (mRemoteDevice == null) {
- return; // Camera already closed
+ if (mRemoteDevice == null && mRemoteDeviceInit) {
+ return; // Camera already closed, user is not interested in errors anymore.
}
// Redirect device callback to the offline session in case we are in the middle
diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
index 956a474..d64009c 100644
--- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
+++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
@@ -1223,14 +1223,6 @@
new StreamCombinationTemplate(new StreamTemplate [] {
new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
STREAM_USE_CASE_PREVIEW),
- new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.RECORD,
- STREAM_USE_CASE_RECORD),
- new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
- STREAM_USE_CASE_STILL_CAPTURE)},
- "Preview, video record and YUV video snapshot"),
- new StreamCombinationTemplate(new StreamTemplate [] {
- new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
- STREAM_USE_CASE_PREVIEW),
new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
STREAM_USE_CASE_RECORD),
new StreamTemplate(ImageFormat.JPEG, SizeThreshold.RECORD,
@@ -1239,27 +1231,11 @@
new StreamCombinationTemplate(new StreamTemplate [] {
new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
STREAM_USE_CASE_PREVIEW),
- new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
- STREAM_USE_CASE_RECORD),
- new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.RECORD,
- STREAM_USE_CASE_STILL_CAPTURE)},
- "Preview, in-application video processing and YUV video snapshot"),
- new StreamCombinationTemplate(new StreamTemplate [] {
- new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
- STREAM_USE_CASE_PREVIEW),
new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
STREAM_USE_CASE_PREVIEW),
new StreamTemplate(ImageFormat.JPEG, SizeThreshold.MAXIMUM,
STREAM_USE_CASE_STILL_CAPTURE)},
"Preview, in-application image processing, and JPEG still image capture"),
- new StreamCombinationTemplate(new StreamTemplate [] {
- new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
- STREAM_USE_CASE_PREVIEW),
- new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
- STREAM_USE_CASE_PREVIEW),
- new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.MAXIMUM,
- STREAM_USE_CASE_STILL_CAPTURE)},
- "Preview, in-application image processing, and YUV still image capture"),
};
private static StreamCombinationTemplate sPreviewStabilizedStreamCombinations[] = {
diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java
index dba1a5e..6a667fe 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManager.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManager.java
@@ -251,6 +251,10 @@
@Nullable
private Boolean lastResult;
+ public FoldStateListener(Context context) {
+ this(context, folded -> {});
+ }
+
public FoldStateListener(Context context, Consumer<Boolean> listener) {
mFoldedDeviceStates = context.getResources().getIntArray(
com.android.internal.R.array.config_foldedDeviceStates);
@@ -266,5 +270,10 @@
mDelegate.accept(folded);
}
}
+
+ @Nullable
+ public Boolean getFolded() {
+ return lastResult;
+ }
}
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index baf8836..b6cd06e 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -168,7 +168,7 @@
* The <code>android:label</code> attribute specifies a human-readable descriptive
* label to describe the keyboard layout in the user interface, such as "English (US)".
* The <code>android:keyboardLayout</code> attribute refers to a
- * <a href="http://source.android.com/tech/input/key-character-map-files.html">
+ * <a href="https://source.android.com/docs/core/interaction/input/key-character-map-files">
* key character map</a> resource that defines the keyboard layout.
* </p>
*/
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index e1d15de..125bdaf 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -74,6 +74,7 @@
String getUserAccount(int userId);
void setUserAccount(int userId, String accountName);
long getUserCreationTime(int userId);
+ int getUserSwitchability(int userId);
boolean isUserSwitcherEnabled(int mUserId);
boolean isRestricted(int userId);
boolean canHaveRestrictedProfile(int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index e5b8472..df82a54 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -58,7 +58,6 @@
import android.graphics.drawable.Drawable;
import android.location.LocationManager;
import android.provider.Settings;
-import android.telephony.TelephonyManager;
import android.util.AndroidException;
import android.util.ArraySet;
import android.util.Log;
@@ -595,8 +594,11 @@
/**
* Specifies if a user is disallowed from transferring files over USB.
*
- * <p>This restriction can only be set by a device owner, a profile owner on the primary
- * user or a profile owner of an organization-owned managed profile on the parent profile.
+ * <p>This restriction can only be set by a <a href="https://developers.google.com/android/work/terminology#device_owner_do">
+ * device owner</a> or a <a href="https://developers.google.com/android/work/terminology#profile_owner_po">
+ * profile owner</a> on the primary user's profile or a profile owner of an organization-owned
+ * <a href="https://developers.google.com/android/work/terminology#managed_profile">
+ * managed profile</a> on the parent profile.
* When it is set by a device owner, it applies globally. When it is set by a profile owner
* on the primary user or by a profile owner of an organization-owned managed profile on
* the parent profile, it disables the primary user from transferring files over USB. No other
@@ -1687,7 +1689,7 @@
public static final int SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED = 1 << 2;
/**
- * Result returned in {@link #getUserSwitchability()} indicating user swichability.
+ * Result returned in {@link #getUserSwitchability()} indicating user switchability.
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@@ -2054,25 +2056,16 @@
* @hide
*/
@Deprecated
- @RequiresPermission(allOf = {
- Manifest.permission.READ_PHONE_STATE,
- Manifest.permission.MANAGE_USERS}, // Can be INTERACT_ACROSS_USERS instead.
- conditional = true)
+ @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+ android.Manifest.permission.INTERACT_ACROSS_USERS})
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@UserHandleAware
public boolean canSwitchUsers() {
- boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
- boolean isSystemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
- boolean inCall = false;
- TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
- if (telephonyManager != null) {
- inCall = telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE;
+ try {
+ return mService.getUserSwitchability(mUserId) == SWITCHABILITY_STATUS_OK;
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
}
- boolean isUserSwitchDisallowed = hasUserRestrictionForUser(DISALLOW_USER_SWITCH, mUserId);
- return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall
- && !isUserSwitchDisallowed;
}
/**
@@ -2106,34 +2099,14 @@
* @return A {@link UserSwitchabilityResult} flag indicating if the user is switchable.
* @hide
*/
- @RequiresPermission(allOf = {Manifest.permission.READ_PHONE_STATE,
- android.Manifest.permission.MANAGE_USERS,
- android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+ android.Manifest.permission.INTERACT_ACROSS_USERS})
public @UserSwitchabilityResult int getUserSwitchability(UserHandle userHandle) {
- final TelephonyManager tm =
- (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-
- int flags = SWITCHABILITY_STATUS_OK;
- if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
- flags |= SWITCHABILITY_STATUS_USER_IN_CALL;
+ try {
+ return mService.getUserSwitchability(userHandle.getIdentifier());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
}
- if (hasUserRestrictionForUser(DISALLOW_USER_SWITCH, userHandle)) {
- flags |= SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
- }
-
- // System User is always unlocked in Headless System User Mode, so ignore this flag
- if (!isHeadlessSystemUserMode()) {
- final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
- final boolean systemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
-
- if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
- flags |= SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
- }
- }
-
- return flags;
}
/**
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 08de87e..0d9f500c 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -2552,6 +2552,8 @@
* This API doesn't require any special permissions, though typical implementations
* will require being called from an SELinux domain that allows setting file attributes
* related to quota (eg the GID or project ID).
+ * If the calling user has MANAGE_EXTERNAL_STORAGE permissions, quota for shared profile's
+ * volumes is also updated.
*
* The default platform user of this API is the MediaProvider process, which is
* responsible for managing all of external storage.
@@ -2572,7 +2574,14 @@
@QuotaType int quotaType) throws IOException {
long projectId;
final String filePath = path.getCanonicalPath();
- final StorageVolume volume = getStorageVolume(path);
+ int volFlags = FLAG_REAL_STATE | FLAG_INCLUDE_INVISIBLE;
+ // If caller has MANAGE_EXTERNAL_STORAGE permission, results from User Profile(s) are also
+ // returned by enabling FLAG_INCLUDE_SHARED_PROFILE.
+ if (mContext.checkSelfPermission(MANAGE_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
+ volFlags |= FLAG_INCLUDE_SHARED_PROFILE;
+ }
+ final StorageVolume[] availableVolumes = getVolumeList(mContext.getUserId(), volFlags);
+ final StorageVolume volume = getStorageVolume(availableVolumes, path);
if (volume == null) {
Log.w(TAG, "Failed to update quota type for " + filePath);
return;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 85e3aee..b4a33ff 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9233,6 +9233,14 @@
public static final int DOCK_SETUP_PROMPTED = 3;
/**
+ * Indicates that the user has started dock setup but never finished it.
+ * One of the possible states for {@link #DOCK_SETUP_STATE}.
+ *
+ * @hide
+ */
+ public static final int DOCK_SETUP_INCOMPLETE = 4;
+
+ /**
* Indicates that the user has completed dock setup.
* One of the possible states for {@link #DOCK_SETUP_STATE}.
*
@@ -9240,6 +9248,14 @@
*/
public static final int DOCK_SETUP_COMPLETED = 10;
+ /**
+ * Indicates that dock setup timed out before the user could complete it.
+ * One of the possible states for {@link #DOCK_SETUP_STATE}.
+ *
+ * @hide
+ */
+ public static final int DOCK_SETUP_TIMED_OUT = 11;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -9247,7 +9263,9 @@
DOCK_SETUP_STARTED,
DOCK_SETUP_PAUSED,
DOCK_SETUP_PROMPTED,
- DOCK_SETUP_COMPLETED
+ DOCK_SETUP_INCOMPLETE,
+ DOCK_SETUP_COMPLETED,
+ DOCK_SETUP_TIMED_OUT
})
public @interface DockSetupState {
}
@@ -9516,7 +9534,7 @@
/**
* Indicates whether "seen" notifications should be suppressed from the lockscreen.
* <p>
- * Type: int (0 for false, 1 for true)
+ * Type: int (0 for unset, 1 for true, 2 for false)
*
* @hide
*/
@@ -9819,11 +9837,12 @@
"fingerprint_side_fps_auth_downtime";
/**
- * Whether or not a SFPS device is required to be interactive for auth to unlock the device.
+ * Whether or not a SFPS device is enabling the performant auth setting.
+ * The "_V2" suffix was added to re-introduce the default behavior for
+ * users. See b/265264294 fore more details.
* @hide
*/
- public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED =
- "sfps_require_screen_on_to_auth_enabled";
+ public static final String SFPS_PERFORMANT_AUTH_ENABLED = "sfps_performant_auth_enabled_v2";
/**
* Whether or not debugging is enabled.
diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java
index 4f37cd9..a2ffa5d 100644
--- a/core/java/android/service/appprediction/AppPredictionService.java
+++ b/core/java/android/service/appprediction/AppPredictionService.java
@@ -328,7 +328,7 @@
Slog.e(TAG, "Callback is null, likely the binder has died.");
return false;
}
- return mCallback.equals(callback);
+ return mCallback.asBinder().equals(callback.asBinder());
}
public void destroy() {
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index 0fb9f57..b0e847c 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -166,7 +166,7 @@
}
/**
- * Description of an event that occured after the latest call to
+ * Description of an event that occurred after the latest call to
* {@link FillCallback#onSuccess(FillResponse)}.
*/
public static final class Event {
diff --git a/core/java/android/service/chooser/ChooserAction.java b/core/java/android/service/chooser/ChooserAction.java
index 3010049..a61b781 100644
--- a/core/java/android/service/chooser/ChooserAction.java
+++ b/core/java/android/service/chooser/ChooserAction.java
@@ -27,10 +27,9 @@
/**
* A ChooserAction is an app-defined action that can be provided to the Android Sharesheet to
- * be shown to the user when {@link android.content.Intent.ACTION_CHOOSER} is invoked.
+ * be shown to the user when {@link android.content.Intent#ACTION_CHOOSER} is invoked.
*
- * @see android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
- * @see android.content.Intent.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION
+ * @see android.content.Intent#EXTRA_CHOOSER_CUSTOM_ACTIONS
* @hide
*/
public final class ChooserAction implements Parcelable {
@@ -88,6 +87,7 @@
return "ChooserAction {" + "label=" + mLabel + ", intent=" + mAction + "}";
}
+ @NonNull
public static final Parcelable.Creator<ChooserAction> CREATOR =
new Creator<ChooserAction>() {
@Override
@@ -137,6 +137,7 @@
* object.
* @return the built action
*/
+ @NonNull
public ChooserAction build() {
return new ChooserAction(mIcon, mLabel, mAction);
}
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 6e8198b..6e4535b 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -36,33 +36,100 @@
public abstract class DreamOverlayService extends Service {
private static final String TAG = "DreamOverlayService";
private static final boolean DEBUG = false;
- private boolean mShowComplications;
- private ComponentName mDreamComponent;
- private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
+ // The last client that started dreaming and hasn't ended
+ private OverlayClient mCurrentClient;
+
+ // An {@link IDreamOverlayClient} implementation that identifies itself when forwarding
+ // requests to the {@link DreamOverlayService}
+ private static class OverlayClient extends IDreamOverlayClient.Stub {
+ private final DreamOverlayService mService;
+ private boolean mShowComplications;
+ private ComponentName mDreamComponent;
+ IDreamOverlayCallback mDreamOverlayCallback;
+
+ OverlayClient(DreamOverlayService service) {
+ mService = service;
+ }
+
@Override
- public void startDream(WindowManager.LayoutParams layoutParams,
- IDreamOverlayCallback callback, String dreamComponent,
- boolean shouldShowComplications) {
- mDreamOverlayCallback = callback;
+ public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback,
+ String dreamComponent, boolean shouldShowComplications) throws RemoteException {
mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
mShowComplications = shouldShowComplications;
- onStartDream(layoutParams);
+ mDreamOverlayCallback = callback;
+ mService.startDream(this, params);
}
+
+
@Override
public void wakeUp() {
- onWakeUp(() -> {
+ mService.wakeUp(this, () -> {
try {
mDreamOverlayCallback.onWakeUpComplete();
} catch (RemoteException e) {
- Log.e(TAG, "Could not notify dream of wakeUp:" + e);
+ Log.e(TAG, "Could not notify dream of wakeUp", e);
}
});
}
- };
- IDreamOverlayCallback mDreamOverlayCallback;
+ @Override
+ public void endDream() {
+ mService.endDream(this);
+ }
+
+ private void onExitRequested() {
+ try {
+ mDreamOverlayCallback.onExitRequested();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not request exit:" + e);
+ }
+ }
+
+ private boolean shouldShowComplications() {
+ return mShowComplications;
+ }
+
+ private ComponentName getComponent() {
+ return mDreamComponent;
+ }
+ }
+
+ private void startDream(OverlayClient client, WindowManager.LayoutParams params) {
+ endDream(mCurrentClient);
+ mCurrentClient = client;
+ onStartDream(params);
+ }
+
+ private void endDream(OverlayClient client) {
+ if (client == null || client != mCurrentClient) {
+ return;
+ }
+
+ onEndDream();
+ mCurrentClient = null;
+ }
+
+ private void wakeUp(OverlayClient client, Runnable callback) {
+ if (mCurrentClient != client) {
+ return;
+ }
+
+ onWakeUp(callback);
+ }
+
+ private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
+ @Override
+ public void getClient(IDreamOverlayClientCallback callback) {
+ try {
+ callback.onDreamOverlayClient(
+ new OverlayClient(DreamOverlayService.this));
+ } catch (RemoteException e) {
+ Log.e(TAG, "could not send client to callback", e);
+ }
+ }
+ };
public DreamOverlayService() {
}
@@ -83,31 +150,45 @@
/**
* This method is overridden by implementations to handle when the dream has been requested
- * to wakeup. This allows any overlay animations to run.
+ * to wakeup. This allows any overlay animations to run. By default, the method will invoke
+ * the callback immediately.
*
* @param onCompleteCallback The callback to trigger to notify the dream service that the
* overlay has completed waking up.
* @hide
*/
public void onWakeUp(@NonNull Runnable onCompleteCallback) {
+ onCompleteCallback.run();
+ }
+
+ /**
+ * This method is overridden by implementations to handle when the dream has ended. There may
+ * be earlier signals leading up to this step, such as @{@link #onWakeUp(Runnable)}.
+ */
+ public void onEndDream() {
}
/**
* This method is invoked to request the dream exit.
*/
public final void requestExit() {
- try {
- mDreamOverlayCallback.onExitRequested();
- } catch (RemoteException e) {
- Log.e(TAG, "Could not request exit:" + e);
+ if (mCurrentClient == null) {
+ throw new IllegalStateException("requested exit with no dream present");
}
+
+ mCurrentClient.onExitRequested();
}
/**
* Returns whether to show complications on the dream overlay.
*/
public final boolean shouldShowComplications() {
- return mShowComplications;
+ if (mCurrentClient == null) {
+ throw new IllegalStateException(
+ "requested if should show complication when no dream active");
+ }
+
+ return mCurrentClient.shouldShowComplications();
}
/**
@@ -115,6 +196,10 @@
* @hide
*/
public final ComponentName getDreamComponent() {
- return mDreamComponent;
+ if (mCurrentClient == null) {
+ throw new IllegalStateException("requested dream component when no dream active");
+ }
+
+ return mCurrentClient.getComponent();
}
}
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 8b9852a..6a4710f 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -248,25 +248,39 @@
private OverlayConnection mOverlayConnection;
private static class OverlayConnection extends PersistentServiceConnection<IDreamOverlay> {
- // Overlay set during onBind.
- private IDreamOverlay mOverlay;
+ // Retrieved Client
+ private IDreamOverlayClient mClient;
+
// A list of pending requests to execute on the overlay.
- private final ArrayList<Consumer<IDreamOverlay>> mConsumers = new ArrayList<>();
+ private final ArrayList<Consumer<IDreamOverlayClient>> mConsumers = new ArrayList<>();
+
+ private final IDreamOverlayClientCallback mClientCallback =
+ new IDreamOverlayClientCallback.Stub() {
+ @Override
+ public void onDreamOverlayClient(IDreamOverlayClient client) {
+ mClient = client;
+
+ for (Consumer<IDreamOverlayClient> consumer : mConsumers) {
+ consumer.accept(mClient);
+ }
+ }
+ };
private final Callback<IDreamOverlay> mCallback = new Callback<IDreamOverlay>() {
@Override
public void onConnected(ObservableServiceConnection<IDreamOverlay> connection,
IDreamOverlay service) {
- mOverlay = service;
- for (Consumer<IDreamOverlay> consumer : mConsumers) {
- consumer.accept(mOverlay);
+ try {
+ service.getClient(mClientCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "could not get DreamOverlayClient", e);
}
}
@Override
public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection,
int reason) {
- mOverlay = null;
+ mClient = null;
}
};
@@ -296,15 +310,21 @@
super.unbind();
}
- public void addConsumer(Consumer<IDreamOverlay> consumer) {
- mConsumers.add(consumer);
- if (mOverlay != null) {
- consumer.accept(mOverlay);
- }
+ public void addConsumer(Consumer<IDreamOverlayClient> consumer) {
+ execute(() -> {
+ mConsumers.add(consumer);
+ if (mClient != null) {
+ consumer.accept(mClient);
+ }
+ });
}
- public void removeConsumer(Consumer<IDreamOverlay> consumer) {
- mConsumers.remove(consumer);
+ public void removeConsumer(Consumer<IDreamOverlayClient> consumer) {
+ execute(() -> mConsumers.remove(consumer));
+ }
+
+ public void clearConsumers() {
+ execute(() -> mConsumers.clear());
}
}
@@ -1044,6 +1064,24 @@
* </p>
*/
public final void finish() {
+ // If there is an active overlay connection, signal that the dream is ending before
+ // continuing. Note that the overlay cannot rely on the unbound state, since another dream
+ // might have bound to it in the meantime.
+ if (mOverlayConnection != null) {
+ mOverlayConnection.addConsumer(overlay -> {
+ try {
+ overlay.endDream();
+ mOverlayConnection.unbind();
+ mOverlayConnection = null;
+ finish();
+ } catch (RemoteException e) {
+ Log.e(mTag, "could not inform overlay of dream end:" + e);
+ }
+ });
+ mOverlayConnection.clearConsumers();
+ return;
+ }
+
if (mDebug) Slog.v(mTag, "finish(): mFinished=" + mFinished);
Activity activity = mActivity;
@@ -1060,10 +1098,6 @@
}
mFinished = true;
- if (mOverlayConnection != null) {
- mOverlayConnection.unbind();
- }
-
if (mDreamToken == null) {
if (mDebug) Slog.v(mTag, "finish() called when not attached.");
stopSelf();
@@ -1359,7 +1393,7 @@
mWindow.getDecorView().addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
- private Consumer<IDreamOverlay> mDreamStartOverlayConsumer;
+ private Consumer<IDreamOverlayClient> mDreamStartOverlayConsumer;
@Override
public void onViewAttachedToWindow(View v) {
@@ -1391,6 +1425,7 @@
mActivity = null;
finish();
}
+
if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) {
mOverlayConnection.removeConsumer(mDreamStartOverlayConsumer);
}
diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl
index 7aeceb2c..7ec75a5 100644
--- a/core/java/android/service/dreams/IDreamOverlay.aidl
+++ b/core/java/android/service/dreams/IDreamOverlay.aidl
@@ -16,8 +16,7 @@
package android.service.dreams;
-import android.service.dreams.IDreamOverlayCallback;
-import android.view.WindowManager.LayoutParams;
+import android.service.dreams.IDreamOverlayClientCallback;
/**
* {@link IDreamOverlay} provides a way for a component to annotate a dream with additional view
@@ -28,17 +27,7 @@
*/
interface IDreamOverlay {
/**
- * @param params The {@link LayoutParams} for the associated DreamWindow, including the window
- token of the Dream Activity.
- * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
- * dream.
- * @param dreamComponent The component name of the dream service requesting overlay.
- * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
- * and weather.
+ * Retrieves a client the caller can use to interact with the dream overlay.
*/
- void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
- in String dreamComponent, in boolean shouldShowComplications);
-
- /** Called when the dream is waking, to do any exit animations */
- void wakeUp();
+ void getClient(in IDreamOverlayClientCallback callback);
}
diff --git a/core/java/android/service/dreams/IDreamOverlayClient.aidl b/core/java/android/service/dreams/IDreamOverlayClient.aidl
new file mode 100644
index 0000000..78b7280
--- /dev/null
+++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.dreams;
+
+import android.service.dreams.IDreamOverlayCallback;
+import android.view.WindowManager.LayoutParams;
+
+/**
+* {@link IDreamOverlayClient} allows {@link DreamService} instances to act upon the dream overlay.
+*
+* @hide
+*/
+interface IDreamOverlayClient {
+ /**
+ * @param params The {@link LayoutParams} for the associated DreamWindow, including the window
+ token of the Dream Activity.
+ * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the
+ * dream.
+ * @param dreamComponent The component name of the dream service requesting overlay.
+ * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock
+ * and weather.
+ */
+ void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
+ in String dreamComponent, in boolean shouldShowComplications);
+
+ /** Called when the dream is waking, to do any exit animations */
+ void wakeUp();
+
+ /** Called when the dream has ended. */
+ void endDream();
+}
diff --git a/core/java/android/service/dreams/IDreamOverlayClientCallback.aidl b/core/java/android/service/dreams/IDreamOverlayClientCallback.aidl
new file mode 100644
index 0000000..244d999
--- /dev/null
+++ b/core/java/android/service/dreams/IDreamOverlayClientCallback.aidl
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.dreams;
+
+import android.service.dreams.IDreamOverlayClient;
+
+/**
+* {@link IDreamOverlayClientCallback} allows receiving a requested {@link IDreamOverlayClient}.
+* @hide
+*/
+interface IDreamOverlayClientCallback {
+ /**
+ * Called with a unique {@link IDreamOverlayClient}.
+ */
+ void onDreamOverlayClient(in IDreamOverlayClient client);
+}
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index 4b25c88..182a497 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -52,7 +52,7 @@
/** @hide */
@StringDef (prefix = { "KEY_" }, value = {
KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA,
- KEY_TEXT_REPLIES, KEY_USER_SENTIMENT
+ KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL
})
@Retention(RetentionPolicy.SOURCE)
public @interface Keys {}
@@ -122,6 +122,19 @@
public static final String KEY_IMPORTANCE = "key_importance";
/**
+ * Weaker than {@link #KEY_IMPORTANCE}, this adjustment suggests an importance rather than
+ * mandates an importance change.
+ *
+ * A notification listener can interpet this suggestion to show the user a prompt to change
+ * notification importance for the notification (or type, or app) moving forward.
+ *
+ * Data type: int, one of importance values e.g.
+ * {@link android.app.NotificationManager#IMPORTANCE_MIN}.
+ * @hide
+ */
+ public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal";
+
+ /**
* Data type: float, a ranking score from 0 (lowest) to 1 (highest).
* Used to rank notifications inside that fall under the same classification (i.e. alerting,
* silenced).
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index ad2e9d5..dc4cb9f 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -1711,6 +1711,8 @@
private ShortcutInfo mShortcutInfo;
private @RankingAdjustment int mRankingAdjustment;
private boolean mIsBubble;
+ // Notification assistant importance suggestion
+ private int mProposedImportance;
private static final int PARCEL_VERSION = 2;
@@ -1748,6 +1750,7 @@
out.writeParcelable(mShortcutInfo, flags);
out.writeInt(mRankingAdjustment);
out.writeBoolean(mIsBubble);
+ out.writeInt(mProposedImportance);
}
/** @hide */
@@ -1786,6 +1789,7 @@
mShortcutInfo = in.readParcelable(cl, android.content.pm.ShortcutInfo.class);
mRankingAdjustment = in.readInt();
mIsBubble = in.readBoolean();
+ mProposedImportance = in.readInt();
}
@@ -1878,6 +1882,22 @@
}
/**
+ * Returns the proposed importance provided by the {@link NotificationAssistantService}.
+ *
+ * This can be used to suggest that the user change the importance of this type of
+ * notification moving forward. A value of
+ * {@link NotificationManager#IMPORTANCE_UNSPECIFIED} means that the NAS has not recommended
+ * a change to the importance, and no UI should be shown to the user. See
+ * {@link Adjustment#KEY_IMPORTANCE_PROPOSAL}.
+ *
+ * @return the importance of the notification
+ * @hide
+ */
+ public @NotificationManager.Importance int getProposedImportance() {
+ return mProposedImportance;
+ }
+
+ /**
* If the system has overridden the group key, then this will be non-null, and this
* key should be used to bundle notifications.
*/
@@ -2041,7 +2061,7 @@
boolean noisy, ArrayList<Notification.Action> smartActions,
ArrayList<CharSequence> smartReplies, boolean canBubble,
boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo,
- int rankingAdjustment, boolean isBubble) {
+ int rankingAdjustment, boolean isBubble, int proposedImportance) {
mKey = key;
mRank = rank;
mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
@@ -2067,6 +2087,7 @@
mShortcutInfo = shortcutInfo;
mRankingAdjustment = rankingAdjustment;
mIsBubble = isBubble;
+ mProposedImportance = proposedImportance;
}
/**
@@ -2107,7 +2128,8 @@
other.mIsConversation,
other.mShortcutInfo,
other.mRankingAdjustment,
- other.mIsBubble);
+ other.mIsBubble,
+ other.mProposedImportance);
}
/**
@@ -2166,7 +2188,8 @@
&& Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()),
(other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId()))
&& Objects.equals(mRankingAdjustment, other.mRankingAdjustment)
- && Objects.equals(mIsBubble, other.mIsBubble);
+ && Objects.equals(mIsBubble, other.mIsBubble)
+ && Objects.equals(mProposedImportance, other.mProposedImportance);
}
}
diff --git a/core/java/android/service/quickaccesswallet/WalletCard.java b/core/java/android/service/quickaccesswallet/WalletCard.java
index b09d2e9..7aacb9b 100644
--- a/core/java/android/service/quickaccesswallet/WalletCard.java
+++ b/core/java/android/service/quickaccesswallet/WalletCard.java
@@ -16,6 +16,7 @@
package android.service.quickaccesswallet;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
@@ -24,28 +25,73 @@
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
/**
* A {@link WalletCard} can represent anything that a user might carry in their wallet -- a credit
* card, library card, transit pass, etc. Cards are identified by a String identifier and contain a
- * card image, card image content description, and a {@link PendingIntent} to be used if the user
- * clicks on the card. Cards may be displayed with an icon and label, though these are optional.
+ * card type, card image, card image content description, and a {@link PendingIntent} to be used if
+ * the user clicks on the card. Cards may be displayed with an icon and label, though these are
+ * optional. Valuable cards will also have a second image that will be displayed when the card is
+ * tapped.
*/
+
public final class WalletCard implements Parcelable {
+ /**
+ * Unknown cards refer to cards whose types are unspecified.
+ * @hide
+ */
+ public static final int CARD_TYPE_UNKNOWN = 0;
+
+ /**
+ * Payment cards refer to credit cards, debit cards or any other cards in the wallet used to
+ * make cash-equivalent payments.
+ * @hide
+ */
+ public static final int CARD_TYPE_PAYMENT = 1;
+
+ /**
+ * Valuable cards refer to any cards that are not used for cash-equivalent payment.
+ * This includes event tickets, flights, offers, loyalty cards, gift cards and transit tickets.
+ * @hide
+ */
+ public static final int CARD_TYPE_VALUABLE = 2;
+
private final String mCardId;
+ private final int mCardType;
private final Icon mCardImage;
private final CharSequence mContentDescription;
private final PendingIntent mPendingIntent;
private final Icon mCardIcon;
private final CharSequence mCardLabel;
+ private final Icon mValuableCardSecondaryImage;
private WalletCard(Builder builder) {
this.mCardId = builder.mCardId;
+ this.mCardType = builder.mCardType;
this.mCardImage = builder.mCardImage;
this.mContentDescription = builder.mContentDescription;
this.mPendingIntent = builder.mPendingIntent;
this.mCardIcon = builder.mCardIcon;
this.mCardLabel = builder.mCardLabel;
+ this.mValuableCardSecondaryImage = builder.mValuableCardSecondaryImage;
+ }
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"CARD_TYPE_"}, value = {
+ CARD_TYPE_UNKNOWN,
+ CARD_TYPE_PAYMENT,
+ CARD_TYPE_VALUABLE
+ })
+ public @interface CardType {
}
@Override
@@ -56,29 +102,44 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(mCardId);
+ dest.writeInt(mCardType);
mCardImage.writeToParcel(dest, flags);
TextUtils.writeToParcel(mContentDescription, dest, flags);
PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
- if (mCardIcon == null) {
+ writeIconIfNonNull(mCardIcon, dest, flags);
+ TextUtils.writeToParcel(mCardLabel, dest, flags);
+ writeIconIfNonNull(mValuableCardSecondaryImage, dest, flags);
+
+ }
+
+ /** Utility function called by writeToParcel
+ */
+ private void writeIconIfNonNull(Icon icon, Parcel dest, int flags) {
+ if (icon == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
- mCardIcon.writeToParcel(dest, flags);
+ icon.writeToParcel(dest, flags);
}
- TextUtils.writeToParcel(mCardLabel, dest, flags);
}
private static WalletCard readFromParcel(Parcel source) {
String cardId = source.readString();
+ int cardType = source.readInt();
Icon cardImage = Icon.CREATOR.createFromParcel(source);
CharSequence contentDesc = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
PendingIntent pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(source);
Icon cardIcon = source.readByte() == 0 ? null : Icon.CREATOR.createFromParcel(source);
CharSequence cardLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
- return new Builder(cardId, cardImage, contentDesc, pendingIntent)
+ Icon valuableCardSecondaryImage = source.readByte() == 0 ? null :
+ Icon.CREATOR.createFromParcel(source);
+ Builder builder = new Builder(cardId, cardType, cardImage, contentDesc, pendingIntent)
.setCardIcon(cardIcon)
- .setCardLabel(cardLabel)
- .build();
+ .setCardLabel(cardLabel);
+
+ return cardType == CARD_TYPE_VALUABLE
+ ? builder.setValuableCardSecondaryImage(valuableCardSecondaryImage).build() :
+ builder.build();
}
@NonNull
@@ -104,6 +165,16 @@
}
/**
+ * Returns the card type.
+ * @hide
+ */
+ @NonNull
+ @CardType
+ public int getCardType() {
+ return mCardType;
+ }
+
+ /**
* The visual representation of the card. If the card image Icon is a bitmap, it should have a
* width of {@link GetWalletCardsRequest#getCardWidthPx()} and a height of {@link
* GetWalletCardsRequest#getCardHeightPx()}.
@@ -158,23 +229,37 @@
}
/**
- * Builder for {@link WalletCard} objects. You must to provide cardId, cardImage,
+ * Visual representation of the card when it is tapped. Includes a barcode to scan the card in
+ * addition to the information in the primary image.
+ * @hide
+ */
+ @Nullable
+ public Icon getValuableCardSecondaryImage() {
+ return mValuableCardSecondaryImage;
+ }
+
+ /**
+ * Builder for {@link WalletCard} objects. You must provide cardId, cardImage,
* contentDescription, and pendingIntent. If the card is opaque and should be shown with
* elevation, set hasShadow to true. cardIcon and cardLabel are optional.
*/
public static final class Builder {
private String mCardId;
+ private int mCardType;
private Icon mCardImage;
private CharSequence mContentDescription;
private PendingIntent mPendingIntent;
private Icon mCardIcon;
private CharSequence mCardLabel;
+ private Icon mValuableCardSecondaryImage;
/**
* @param cardId The card id must be non-null and unique within the list of
* cards returned. <b>Note:
* </b> this card ID should <b>not</b> contain PII (Personally
* Identifiable Information, such as username or email address).
+ * @param cardType Integer representing the card type. The card type must be
+ * non-null. If not provided, it defaults to unknown.
* @param cardImage The visual representation of the card. If the card image Icon
* is a bitmap, it should have a width of {@link
* GetWalletCardsRequest#getCardWidthPx()} and a height of {@link
@@ -193,15 +278,30 @@
* request device unlock before sending the pending intent. It is
* recommended that the pending intent be immutable (use {@link
* PendingIntent#FLAG_IMMUTABLE}).
+ * @hide
+ */
+ public Builder(@NonNull String cardId,
+ @NonNull @CardType int cardType,
+ @NonNull Icon cardImage,
+ @NonNull CharSequence contentDescription,
+ @NonNull PendingIntent pendingIntent
+ ) {
+ mCardId = cardId;
+ mCardType = cardType;
+ mCardImage = cardImage;
+ mContentDescription = contentDescription;
+ mPendingIntent = pendingIntent;
+ }
+
+ /**
+ * Called when a card type is not provided.
*/
public Builder(@NonNull String cardId,
@NonNull Icon cardImage,
@NonNull CharSequence contentDescription,
@NonNull PendingIntent pendingIntent) {
- mCardId = cardId;
- mCardImage = cardImage;
- mContentDescription = contentDescription;
- mPendingIntent = pendingIntent;
+ this(cardId, WalletCard.CARD_TYPE_UNKNOWN, cardImage, contentDescription,
+ pendingIntent);
}
/**
@@ -236,6 +336,19 @@
}
/**
+ * Visual representation of the card when it is tapped. Includes a barcode to scan the card
+ * in addition to the information in the primary image.
+ * @hide
+ */
+ @NonNull
+ public Builder setValuableCardSecondaryImage(@Nullable Icon valuableCardSecondaryImage) {
+ Preconditions.checkState(mCardType == CARD_TYPE_VALUABLE,
+ "This field can only be set on valuable cards");
+ mValuableCardSecondaryImage = valuableCardSecondaryImage;
+ return this;
+ }
+
+ /**
* Builds a new {@link WalletCard} instance.
*
* @return A built response.
diff --git a/core/java/android/service/smartspace/SmartspaceService.java b/core/java/android/service/smartspace/SmartspaceService.java
index 3a148df..b13a069 100644
--- a/core/java/android/service/smartspace/SmartspaceService.java
+++ b/core/java/android/service/smartspace/SmartspaceService.java
@@ -302,7 +302,7 @@
Slog.e(TAG, "Callback is null, likely the binder has died.");
return false;
}
- return mCallback.equals(callback);
+ return mCallback.asBinder().equals(callback.asBinder());
}
@Override
diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java
index a1d6cc8..6da0b63 100644
--- a/core/java/android/text/TextShaper.java
+++ b/core/java/android/text/TextShaper.java
@@ -173,7 +173,7 @@
private TextShaper() {}
/**
- * An consumer interface for accepting text shape result.
+ * A consumer interface for accepting text shape result.
*/
public interface GlyphsConsumer {
/**
diff --git a/core/java/android/util/SparseSetArray.java b/core/java/android/util/SparseSetArray.java
index b7873b7..61f29a4 100644
--- a/core/java/android/util/SparseSetArray.java
+++ b/core/java/android/util/SparseSetArray.java
@@ -139,4 +139,9 @@
public T valueAt(int intIndex, int valueIndex) {
return mData.valueAt(intIndex).valueAt(valueIndex);
}
+
+ /** @return The set of values for key at position {@code intIndex}. */
+ public ArraySet<T> valuesAt(int intIndex) {
+ return mData.valueAt(intIndex);
+ }
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 5f04d58..f0a14ae 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -73,6 +73,7 @@
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
@@ -1273,7 +1274,7 @@
mTmpFrames.attachedFrame = attachedFrame;
mTmpFrames.compatScale = compatScale[0];
mInvCompatScale = 1f / compatScale[0];
- } catch (RemoteException e) {
+ } catch (RemoteException | RuntimeException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
@@ -2768,7 +2769,7 @@
* TODO(b/260382739): Apply this to all windows.
*/
private static boolean shouldOptimizeMeasure(final WindowManager.LayoutParams lp) {
- return lp.type == TYPE_NOTIFICATION_SHADE;
+ return (lp.privateFlags & PRIVATE_FLAG_OPTIMIZE_MEASURE) != 0;
}
private Rect getWindowBoundsInsetSystemBars() {
@@ -8749,6 +8750,10 @@
mAdded = false;
AnimationHandler.removeRequestor(this);
}
+ if (mSyncBufferCallback != null) {
+ mSyncBufferCallback.onBufferReady(null);
+ mSyncBufferCallback = null;
+ }
WindowManagerGlobal.getInstance().doRemoveView(this);
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index ed9cb00..f62a487 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -814,8 +814,8 @@
}
/**
- * Activity level {@link android.content.pm.PackageManager.Property PackageManager
- * .Property} for an app to inform the system that the activity can be opted-in or opted-out
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app can be opted-in or opted-out
* from the compatibility treatment that avoids {@link
* android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by
* ignoreRequestedOrientation display setting enabled on the device or by the landscape natural
@@ -833,17 +833,17 @@
* <li>Camera compatibility force rotation treatment is active for the package.
* </ul>
*
- * <p>Setting this property to {@code false} informs the system that the activity must be
+ * <p>Setting this property to {@code false} informs the system that the app must be
* opted-out from the compatibility treatment even if the device manufacturer has opted the app
* into the treatment.
*
* <p><b>Syntax:</b>
* <pre>
- * <activity>
+ * <application>
* <property
* android:name="android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"
* android:value="true|false"/>
- * </activity>
+ * </application>
* </pre>
*
* @hide
@@ -853,6 +853,214 @@
"android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
/**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be excluded from the
+ * camera compatibility force rotation treatment.
+ *
+ * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+ * orientation of the device and set opposite to natural orientation for a landscape app
+ * window. Mismatch between them can lead to camera issues like sideways or stretched
+ * viewfinder since this is one of the strongest assumptions that apps make when they implement
+ * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+ * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+ * camera and is removed once camera is closed.
+ *
+ * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+ * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+ * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+ * for more details).
+ *
+ * <p>With this property set to {@code true} or unset, the system may apply the force rotation
+ * treatment to fixed orientation activities. Device manufacturers can exclude packages from the
+ * treatment using their discretion to improve display compatibility.
+ *
+ * <p>With this property set to {@code false}, the system will not apply the force rotation
+ * treatment.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION =
+ "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION";
+
+ /**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be excluded
+ * from the activity "refresh" after the camera compatibility force rotation treatment.
+ *
+ * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+ * orientation of the device and set opposite to natural orientation for a landscape app
+ * window. Mismatch between them can lead to camera issues like sideways or stretched
+ * viewfinder since this is one of the strongest assumptions that apps make when they implement
+ * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+ * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+ * camera and is removed once camera is closed.
+ *
+ * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed ->
+ * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle
+ * (if overridden, see {@link #PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE} for context).
+ * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+ * camera preview and can lead to sideways or stretching issues persisting even after force
+ * rotation.
+ *
+ * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+ * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+ * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+ * for more details).
+ *
+ * <p>With this property set to {@code true} or unset, the system may "refresh" activity after
+ * the force rotation treatment. Device manufacturers can exclude packages from the "refresh"
+ * using their discretion to improve display compatibility.
+ *
+ * <p>With this property set to {@code false}, the system will not "refresh" activity after the
+ * force rotation treatment.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH =
+ "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH";
+
+ /**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the activity should be or shouldn't be
+ * "refreshed" after the camera compatibility force rotation treatment using "paused ->
+ * resumed" cycle rather than "stopped -> resumed".
+ *
+ * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+ * orientation of the device and set opposite to natural orientation for a landscape app
+ * window. Mismatch between them can lead to camera issues like sideways or stretched
+ * viewfinder since this is one of the strongest assumptions that apps make when they implement
+ * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+ * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+ * camera and is removed once camera is closed.
+ *
+ * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed ->
+ * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle
+ * (if overridden by device manufacturers or using this property). This allows to clear cached
+ * values in apps (e.g., display or camera rotation) that influence camera preview and can lead
+ * to sideways or stretching issues persisting even after force rotation.
+ *
+ * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+ * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+ * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+ * for more details).
+ *
+ * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed"
+ * cycle using their discretion to improve display compatibility.
+ *
+ * <p>With this property set to {@code true}, the system will "refresh" activity after the
+ * force rotation treatment using "resumed -> paused -> resumed" cycle.
+ *
+ * <p>With this property set to {@code false}, the system will not "refresh" activity after the
+ * force rotation treatment using "resumed -> paused -> resumed" cycle even if the device
+ * manufacturer adds the corresponding override.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE =
+ "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE";
+
+ /**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be excluded from the
+ * compatibility override for orientation set by the device manufacturer.
+ *
+ * <p>With this property set to {@code true} or unset, device manufacturers can override
+ * orientation for the app using their discretion to improve display compatibility.
+ *
+ * <p>With this property set to {@code false}, device manufactured per-app override for
+ * orientation won't be applied.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE =
+ "android.window.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE";
+
+ /**
+ * Application level {@link android.content.pm.PackageManager.Property PackageManager
+ * .Property} for an app to inform the system that the app should be opted-out from the
+ * compatibility override that fixes display orientation to landscape natural orientation when
+ * an activity is fullscreen.
+ *
+ * <p>When this compat override is enabled and while display is fixed to the landscape natural
+ * orientation, the orientation requested by the activity will be still respected by bounds
+ * resolution logic. For instance, if an activity requests portrait orientation, then activity
+ * will appear in the letterbox mode for fixed orientation with the display rotated to the
+ * lanscape natural orientation.
+ *
+ * <p>The treatment is disabled by default but device manufacturers can enable the treatment
+ * using their discretion to improve display compatibility on the displays that have
+ * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+ * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+ * for more details).
+ *
+ * <p>With this property set to {@code true} or unset, the system wiil use landscape display
+ * orientation when the following conditions are met:
+ * <ul>
+ * <li>Natural orientation of the display is landscape
+ * <li>ignoreOrientationRequest display setting is enabled
+ * <li>Activity is fullscreen.
+ * <li>Device manufacturer enabled the treatment.
+ * </ul>
+ *
+ * <p>With this property set to {@code false}, device manufactured per-app override for
+ * display orientation won't be applied.
+ *
+ * <p><b>Syntax:</b>
+ * <pre>
+ * <application>
+ * <property
+ * android:name="android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"
+ * android:value="true|false"/>
+ * </application>
+ * </pre>
+ *
+ * @hide
+ */
+ // TODO(b/263984287): Make this public API.
+ String PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE =
+ "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE";
+
+ /**
* @hide
*/
public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array";
@@ -2443,6 +2651,15 @@
public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
/**
+ * Flag to indicate that the view hierarchy of the window can only be measured when
+ * necessary. If a window size can be known by the LayoutParams, we can use the size to
+ * relayout window, and we don't have to measure the view hierarchy before laying out the
+ * views. This reduces the chances to perform measure.
+ * {@hide}
+ */
+ public static final int PRIVATE_FLAG_OPTIMIZE_MEASURE = 0x00000200;
+
+ /**
* Flag that prevents the wallpaper behind the current window from receiving touch events.
*
* {@hide}
@@ -2644,6 +2861,7 @@
PRIVATE_FLAG_NO_MOVE_ANIMATION,
PRIVATE_FLAG_COMPATIBLE_WINDOW,
PRIVATE_FLAG_SYSTEM_ERROR,
+ PRIVATE_FLAG_OPTIMIZE_MEASURE,
PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
@@ -2704,6 +2922,10 @@
equals = PRIVATE_FLAG_SYSTEM_ERROR,
name = "SYSTEM_ERROR"),
@ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_OPTIMIZE_MEASURE,
+ equals = PRIVATE_FLAG_OPTIMIZE_MEASURE,
+ name = "OPTIMIZE_MEASURE"),
+ @ViewDebug.FlagToString(
mask = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
name = "DISABLE_WALLPAPER_TOUCH_EVENTS"),
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index d067d4b..497f066 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -66,8 +66,7 @@
import java.util.function.Consumer;
/**
- * <p>The {@link ContentCaptureManager} provides additional ways for for apps to
- * integrate with the content capture subsystem.
+ * <p>Provides additional ways for apps to integrate with the content capture subsystem.
*
* <p>Content capture provides real-time, continuous capture of application activity, display and
* events to an intelligence service that is provided by the Android system. The intelligence
diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java
index 11f1b6f1..4c87489 100644
--- a/core/java/android/webkit/WebResourceError.java
+++ b/core/java/android/webkit/WebResourceError.java
@@ -19,7 +19,7 @@
import android.annotation.SystemApi;
/**
- * Encapsulates information about errors occured during loading of web resources. See
+ * Encapsulates information about errors that occurred during loading of web resources. See
* {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)}
*/
public abstract class WebResourceError {
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 1024e2e..4a4f561 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -18,10 +18,8 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
-import android.view.RemoteAnimationTarget;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -52,8 +50,6 @@
@SwipeEdge
private final int mSwipeEdge;
- @Nullable
- private final RemoteAnimationTarget mDepartingAnimationTarget;
/**
* Creates a new {@link BackEvent} instance.
@@ -62,16 +58,12 @@
* @param touchY Absolute Y location of the touch point of this event.
* @param progress Value between 0 and 1 on how far along the back gesture is.
* @param swipeEdge Indicates which edge the swipe starts from.
- * @param departingAnimationTarget The remote animation target of the departing application
- * window.
*/
- public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
- @Nullable RemoteAnimationTarget departingAnimationTarget) {
+ public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) {
mTouchX = touchX;
mTouchY = touchY;
mProgress = progress;
mSwipeEdge = swipeEdge;
- mDepartingAnimationTarget = departingAnimationTarget;
}
private BackEvent(@NonNull Parcel in) {
@@ -79,7 +71,6 @@
mTouchY = in.readFloat();
mProgress = in.readFloat();
mSwipeEdge = in.readInt();
- mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
}
public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() {
@@ -105,7 +96,6 @@
dest.writeFloat(mTouchY);
dest.writeFloat(mProgress);
dest.writeInt(mSwipeEdge);
- dest.writeTypedObject(mDepartingAnimationTarget, flags);
}
/**
@@ -136,16 +126,6 @@
return mSwipeEdge;
}
- /**
- * Returns the {@link RemoteAnimationTarget} of the top departing application window,
- * or {@code null} if the top window should not be moved for the current type of back
- * destination.
- */
- @Nullable
- public RemoteAnimationTarget getDepartingAnimationTarget() {
- return mDepartingAnimationTarget;
- }
-
@Override
public String toString() {
return "BackEvent{"
@@ -153,7 +133,6 @@
+ ", mTouchY=" + mTouchY
+ ", mProgress=" + mProgress
+ ", mSwipeEdge" + mSwipeEdge
- + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
+ "}";
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/core/java/android/window/BackMotionEvent.aidl
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
rename to core/java/android/window/BackMotionEvent.aidl
index 67733e9..7c675c3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/core/java/android/window/BackMotionEvent.aidl
@@ -11,15 +11,12 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.keyguard.shared.model
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package android.window;
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
-)
+/**
+ * @hide
+ */
+parcelable BackMotionEvent;
diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java
new file mode 100644
index 0000000..8012a1c
--- /dev/null
+++ b/core/java/android/window/BackMotionEvent.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.RemoteAnimationTarget;
+
+/**
+ * Object used to report back gesture progress. Holds information about a {@link BackEvent} plus
+ * any {@link RemoteAnimationTarget} the gesture manipulates.
+ *
+ * @see BackEvent
+ * @hide
+ */
+public final class BackMotionEvent implements Parcelable {
+ private final float mTouchX;
+ private final float mTouchY;
+ private final float mProgress;
+
+ @BackEvent.SwipeEdge
+ private final int mSwipeEdge;
+ @Nullable
+ private final RemoteAnimationTarget mDepartingAnimationTarget;
+
+ /**
+ * Creates a new {@link BackMotionEvent} instance.
+ *
+ * @param touchX Absolute X location of the touch point of this event.
+ * @param touchY Absolute Y location of the touch point of this event.
+ * @param progress Value between 0 and 1 on how far along the back gesture is.
+ * @param swipeEdge Indicates which edge the swipe starts from.
+ * @param departingAnimationTarget The remote animation target of the departing
+ * application window.
+ */
+ public BackMotionEvent(float touchX, float touchY, float progress,
+ @BackEvent.SwipeEdge int swipeEdge,
+ @Nullable RemoteAnimationTarget departingAnimationTarget) {
+ mTouchX = touchX;
+ mTouchY = touchY;
+ mProgress = progress;
+ mSwipeEdge = swipeEdge;
+ mDepartingAnimationTarget = departingAnimationTarget;
+ }
+
+ private BackMotionEvent(@NonNull Parcel in) {
+ mTouchX = in.readFloat();
+ mTouchY = in.readFloat();
+ mProgress = in.readFloat();
+ mSwipeEdge = in.readInt();
+ mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
+ }
+
+ @NonNull
+ public static final Creator<BackMotionEvent> CREATOR = new Creator<BackMotionEvent>() {
+ @Override
+ public BackMotionEvent createFromParcel(Parcel in) {
+ return new BackMotionEvent(in);
+ }
+
+ @Override
+ public BackMotionEvent[] newArray(int size) {
+ return new BackMotionEvent[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeFloat(mTouchX);
+ dest.writeFloat(mTouchY);
+ dest.writeFloat(mProgress);
+ dest.writeInt(mSwipeEdge);
+ dest.writeTypedObject(mDepartingAnimationTarget, flags);
+ }
+
+ /**
+ * Returns the progress of a {@link BackEvent}.
+ *
+ * @see BackEvent#getProgress()
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * Returns the absolute X location of the touch point.
+ */
+ public float getTouchX() {
+ return mTouchX;
+ }
+
+ /**
+ * Returns the absolute Y location of the touch point.
+ */
+ public float getTouchY() {
+ return mTouchY;
+ }
+
+ /**
+ * Returns the screen edge that the swipe starts from.
+ */
+ @BackEvent.SwipeEdge
+ public int getSwipeEdge() {
+ return mSwipeEdge;
+ }
+
+ /**
+ * Returns the {@link RemoteAnimationTarget} of the top departing application window,
+ * or {@code null} if the top window should not be moved for the current type of back
+ * destination.
+ */
+ @Nullable
+ public RemoteAnimationTarget getDepartingAnimationTarget() {
+ return mDepartingAnimationTarget;
+ }
+
+ @Override
+ public String toString() {
+ return "BackMotionEvent{"
+ + "mTouchX=" + mTouchX
+ + ", mTouchY=" + mTouchY
+ + ", mProgress=" + mProgress
+ + ", mSwipeEdge" + mSwipeEdge
+ + ", mDepartingAnimationTarget" + mDepartingAnimationTarget
+ + "}";
+ }
+}
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
index dd4385c..38c52e7 100644
--- a/core/java/android/window/BackProgressAnimator.java
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -40,7 +40,7 @@
private final SpringAnimation mSpring;
private ProgressCallback mCallback;
private float mProgress = 0;
- private BackEvent mLastBackEvent;
+ private BackMotionEvent mLastBackEvent;
private boolean mStarted = false;
private void setProgress(float progress) {
@@ -82,9 +82,9 @@
/**
* Sets a new target position for the back progress.
*
- * @param event the {@link BackEvent} containing the latest target progress.
+ * @param event the {@link BackMotionEvent} containing the latest target progress.
*/
- public void onBackProgressed(BackEvent event) {
+ public void onBackProgressed(BackMotionEvent event) {
if (!mStarted) {
return;
}
@@ -95,11 +95,11 @@
/**
* Starts the back progress animation.
*
- * @param event the {@link BackEvent} that started the gesture.
+ * @param event the {@link BackMotionEvent} that started the gesture.
* @param callback the back callback to invoke for the gesture. It will receive back progress
* dispatches as the progress animation updates.
*/
- public void onBackStarted(BackEvent event, ProgressCallback callback) {
+ public void onBackStarted(BackMotionEvent event, ProgressCallback callback) {
reset();
mLastBackEvent = event;
mCallback = callback;
@@ -129,8 +129,7 @@
}
mCallback.onProgressUpdate(
new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
- progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(),
- mLastBackEvent.getDepartingAnimationTarget()));
+ progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge()));
}
}
diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl
index 6af8ddd..159c0e8 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -17,7 +17,7 @@
package android.window;
-import android.window.BackEvent;
+import android.window.BackMotionEvent;
/**
* Interface that wraps a {@link OnBackInvokedCallback} object, to be stored in window manager
@@ -30,18 +30,19 @@
* Called when a back gesture has been started, or back button has been pressed down.
* Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}.
*
- * @param backEvent The {@link BackEvent} containing information about the touch or button press.
+ * @param backMotionEvent The {@link BackMotionEvent} containing information about the touch
+ * or button press.
*/
- void onBackStarted(in BackEvent backEvent);
+ void onBackStarted(in BackMotionEvent backMotionEvent);
/**
* Called on back gesture progress.
* Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}.
*
- * @param backEvent The {@link BackEvent} containing information about the latest touch point
- * and the progress that the back animation should seek to.
+ * @param backMotionEvent The {@link BackMotionEvent} containing information about the latest
+ * touch point and the progress that the back animation should seek to.
*/
- void onBackProgressed(in BackEvent backEvent);
+ void onBackProgressed(in BackMotionEvent backMotionEvent);
/**
* Called when a back gesture or back button press has been cancelled.
diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl
index e6bb1f6..0032b9c 100644
--- a/core/java/android/window/ITaskOrganizerController.aidl
+++ b/core/java/android/window/ITaskOrganizerController.aidl
@@ -40,7 +40,8 @@
void unregisterTaskOrganizer(ITaskOrganizer organizer);
/** Creates a persistent root task in WM for a particular windowing-mode. */
- void createRootTask(int displayId, int windowingMode, IBinder launchCookie);
+ void createRootTask(int displayId, int windowingMode, IBinder launchCookie,
+ boolean removeWithTaskOrganizer);
/** Deletes a persistent root task in WM */
boolean deleteRootTask(in WindowContainerToken task);
diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java
index c9ddf92..203d79a 100644
--- a/core/java/android/window/TaskFragmentCreationParams.java
+++ b/core/java/android/window/TaskFragmentCreationParams.java
@@ -71,20 +71,42 @@
*
* This is needed in case we need to launch a placeholder Activity to split below a transparent
* always-expand Activity.
+ *
+ * This should not be used with {@link #mPairedActivityToken}.
*/
@Nullable
private final IBinder mPairedPrimaryFragmentToken;
+ /**
+ * The Activity token to place the new TaskFragment on top of.
+ * When it is set, the new TaskFragment will be positioned right above the target Activity.
+ * Otherwise, the new TaskFragment will be positioned on the top of the Task by default.
+ *
+ * This is needed in case we need to place an Activity into TaskFragment to launch placeholder
+ * below a transparent always-expand Activity, or when there is another Intent being started in
+ * a TaskFragment above.
+ *
+ * This should not be used with {@link #mPairedPrimaryFragmentToken}.
+ */
+ @Nullable
+ private final IBinder mPairedActivityToken;
+
private TaskFragmentCreationParams(
@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken,
@NonNull IBinder ownerToken, @NonNull Rect initialBounds,
- @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken) {
+ @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken,
+ @Nullable IBinder pairedActivityToken) {
+ if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) {
+ throw new IllegalArgumentException("pairedPrimaryFragmentToken and"
+ + " pairedActivityToken should not be set at the same time.");
+ }
mOrganizer = organizer;
mFragmentToken = fragmentToken;
mOwnerToken = ownerToken;
mInitialBounds.set(initialBounds);
mWindowingMode = windowingMode;
mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken;
+ mPairedActivityToken = pairedActivityToken;
}
@NonNull
@@ -121,6 +143,15 @@
return mPairedPrimaryFragmentToken;
}
+ /**
+ * TODO(b/232476698): remove the hide with adding CTS for this in next release.
+ * @hide
+ */
+ @Nullable
+ public IBinder getPairedActivityToken() {
+ return mPairedActivityToken;
+ }
+
private TaskFragmentCreationParams(Parcel in) {
mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in);
mFragmentToken = in.readStrongBinder();
@@ -128,6 +159,7 @@
mInitialBounds.readFromParcel(in);
mWindowingMode = in.readInt();
mPairedPrimaryFragmentToken = in.readStrongBinder();
+ mPairedActivityToken = in.readStrongBinder();
}
/** @hide */
@@ -139,6 +171,7 @@
mInitialBounds.writeToParcel(dest, flags);
dest.writeInt(mWindowingMode);
dest.writeStrongBinder(mPairedPrimaryFragmentToken);
+ dest.writeStrongBinder(mPairedActivityToken);
}
@NonNull
@@ -164,6 +197,7 @@
+ " initialBounds=" + mInitialBounds
+ " windowingMode=" + mWindowingMode
+ " pairedFragmentToken=" + mPairedPrimaryFragmentToken
+ + " pairedActivityToken=" + mPairedActivityToken
+ "}";
}
@@ -194,6 +228,9 @@
@Nullable
private IBinder mPairedPrimaryFragmentToken;
+ @Nullable
+ private IBinder mPairedActivityToken;
+
public Builder(@NonNull TaskFragmentOrganizerToken organizer,
@NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
mOrganizer = organizer;
@@ -224,6 +261,8 @@
* This is needed in case we need to launch a placeholder Activity to split below a
* transparent always-expand Activity.
*
+ * This should not be used with {@link #setPairedActivityToken}.
+ *
* TODO(b/232476698): remove the hide with adding CTS for this in next release.
* @hide
*/
@@ -233,11 +272,32 @@
return this;
}
+ /**
+ * Sets the Activity token to place the new TaskFragment on top of.
+ * When it is set, the new TaskFragment will be positioned right above the target Activity.
+ * Otherwise, the new TaskFragment will be positioned on the top of the Task by default.
+ *
+ * This is needed in case we need to place an Activity into TaskFragment to launch
+ * placeholder below a transparent always-expand Activity, or when there is another Intent
+ * being started in a TaskFragment above.
+ *
+ * This should not be used with {@link #setPairedPrimaryFragmentToken}.
+ *
+ * TODO(b/232476698): remove the hide with adding CTS for this in next release.
+ * @hide
+ */
+ @NonNull
+ public Builder setPairedActivityToken(@Nullable IBinder activityToken) {
+ mPairedActivityToken = activityToken;
+ return this;
+ }
+
/** Constructs the options to create TaskFragment with. */
@NonNull
public TaskFragmentCreationParams build() {
return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken,
- mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken);
+ mInitialBounds, mWindowingMode, mPairedPrimaryFragmentToken,
+ mPairedActivityToken);
}
}
}
diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java
index bffd4e4..02878f8 100644
--- a/core/java/android/window/TaskOrganizer.java
+++ b/core/java/android/window/TaskOrganizer.java
@@ -152,15 +152,31 @@
* @param windowingMode Windowing mode to put the root task in.
* @param launchCookie Launch cookie to associate with the task so that is can be identified
* when the {@link ITaskOrganizer#onTaskAppeared} callback is called.
+ * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+ public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie,
+ boolean removeWithTaskOrganizer) {
+ try {
+ mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie,
+ removeWithTaskOrganizer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates a persistent root task in WM for a particular windowing-mode.
+ * @param displayId The display to create the root task on.
+ * @param windowingMode Windowing mode to put the root task in.
+ * @param launchCookie Launch cookie to associate with the task so that is can be identified
+ * when the {@link ITaskOrganizer#onTaskAppeared} callback is called.
*/
@RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
@Nullable
public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) {
- try {
- mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ createRootTask(displayId, windowingMode, launchCookie, false /* removeWithTaskOrganizer */);
}
/** Deletes a persistent root task in WM */
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index fda39c1..dd9483a 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -229,19 +229,21 @@
}
@Override
- public void onBackStarted(BackEvent backEvent) {
+ public void onBackStarted(BackMotionEvent backEvent) {
Handler.getMain().post(() -> {
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
mProgressAnimator.onBackStarted(backEvent, event ->
callback.onBackProgressed(event));
- callback.onBackStarted(backEvent);
+ callback.onBackStarted(new BackEvent(
+ backEvent.getTouchX(), backEvent.getTouchY(),
+ backEvent.getProgress(), backEvent.getSwipeEdge()));
}
});
}
@Override
- public void onBackProgressed(BackEvent backEvent) {
+ public void onBackProgressed(BackMotionEvent backEvent) {
Handler.getMain().post(() -> {
final OnBackAnimationCallback callback = getBackAnimationCallback();
if (callback != null) {
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 1fcfe7d..011232f 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2953,12 +2953,24 @@
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
return shouldShowTabs()
- && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- UserHandle.of(UserHandle.myUserId())).getCount() > 0
+ && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() > 0
+ || shouldShowContentPreviewWhenEmpty())
&& shouldShowContentPreview();
}
/**
+ * This method could be used to override the default behavior when we hide the preview area
+ * when the current tab doesn't have any items.
+ *
+ * @return true if we want to show the content preview area even if the tab for the current
+ * user is empty
+ */
+ protected boolean shouldShowContentPreviewWhenEmpty() {
+ return false;
+ }
+
+ /**
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
diff --git a/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java b/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java
index e3cc4f1..d0b5811 100644
--- a/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java
+++ b/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java
@@ -47,7 +47,9 @@
/* num_app_provided_app_targets = 6 */ appProvidedApp,
/* is_workprofile = 7 */ isWorkprofile,
/* previewType = 8 */ typeFromPreviewInt(previewType),
- /* intentType = 9 */ typeFromIntentString(intent));
+ /* intentType = 9 */ typeFromIntentString(intent),
+ /* num_provided_custom_actions = 10 */ 0,
+ /* reselection_action_provided = 11 */ false);
}
@Override
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index f8b764b..19e4ba4 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -209,7 +209,7 @@
* <p>Can only be used if there is a work profile.
* <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
*/
- static final String EXTRA_SELECTED_PROFILE =
+ protected static final String EXTRA_SELECTED_PROFILE =
"com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
/**
@@ -224,8 +224,8 @@
static final String EXTRA_CALLING_USER =
"com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
- static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
- static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+ protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
private BroadcastReceiver mWorkProfileStateReceiver;
private UserHandle mHeaderCreatorUser;
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 4f7f8ba..b9373be 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -567,11 +567,6 @@
public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification";
/**
- * (boolean) Whether the clipboard overlay is enabled.
- */
- public static final String CLIPBOARD_OVERLAY_ENABLED = "clipboard_overlay_enabled";
-
- /**
* (boolean) Whether widget provider info would be saved to / loaded from system persistence
* layer as opposed to individual manifests in respective apps.
*/
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 017bf3f..04fc4a6 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -338,4 +338,11 @@
* @param leftOrTop indicates where the stage split is.
*/
void enterStageSplitFromRunningApp(boolean leftOrTop);
+
+ /**
+ * Shows the media output switcher dialog.
+ *
+ * @param packageName of the session for which the output switcher is shown.
+ */
+ void showMediaOutputSwitcher(String packageName);
}
diff --git a/core/java/com/android/internal/util/ObservableServiceConnection.java b/core/java/com/android/internal/util/ObservableServiceConnection.java
index 3165d29..45256fd 100644
--- a/core/java/com/android/internal/util/ObservableServiceConnection.java
+++ b/core/java/com/android/internal/util/ObservableServiceConnection.java
@@ -165,6 +165,13 @@
}
/**
+ * Executes code on the executor specified at construction.
+ */
+ public void execute(Runnable runnable) {
+ mExecutor.execute(runnable);
+ }
+
+ /**
* Initiate binding to the service.
*
* @return {@code true} if initiating binding succeed, {@code false} if the binding failed or
diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java
index 79c5196..3a393b6 100644
--- a/core/java/com/android/internal/util/ScreenshotHelper.java
+++ b/core/java/com/android/internal/util/ScreenshotHelper.java
@@ -1,7 +1,7 @@
package com.android.internal.util;
import static android.content.Intent.ACTION_USER_SWITCHED;
-import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -11,29 +11,18 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
-import android.graphics.Bitmap;
-import android.graphics.ColorSpace;
-import android.graphics.Insets;
-import android.graphics.ParcelableColorSpace;
-import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
import android.net.Uri;
-import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.WindowManager.ScreenshotSource;
-import android.view.WindowManager.ScreenshotType;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.Objects;
import java.util.function.Consumer;
public class ScreenshotHelper {
@@ -41,212 +30,6 @@
public static final int SCREENSHOT_MSG_URI = 1;
public static final int SCREENSHOT_MSG_PROCESS_COMPLETE = 2;
- /**
- * Describes a screenshot request.
- */
- public static class ScreenshotRequest implements Parcelable {
- @ScreenshotType
- private final int mType;
-
- @ScreenshotSource
- private final int mSource;
-
- private final Bundle mBitmapBundle;
- private final Rect mBoundsInScreen;
- private final Insets mInsets;
- private final int mTaskId;
- private final int mUserId;
- private final ComponentName mTopComponent;
-
-
- public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source) {
- this(type, source, /* topComponent */ null);
- }
-
- public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source,
- ComponentName topComponent) {
- this(type,
- source,
- /* bitmapBundle*/ null,
- /* boundsInScreen */ null,
- /* insets */ null,
- /* taskId */ -1,
- /* userId */ -1,
- topComponent);
- }
-
- public ScreenshotRequest(@ScreenshotType int type, @ScreenshotSource int source,
- Bundle bitmapBundle, Rect boundsInScreen, Insets insets, int taskId, int userId,
- ComponentName topComponent) {
- mType = type;
- mSource = source;
- mBitmapBundle = bitmapBundle;
- mBoundsInScreen = boundsInScreen;
- mInsets = insets;
- mTaskId = taskId;
- mUserId = userId;
- mTopComponent = topComponent;
- }
-
- ScreenshotRequest(Parcel in) {
- mType = in.readInt();
- mSource = in.readInt();
- if (in.readInt() == 1) {
- mBitmapBundle = in.readBundle(getClass().getClassLoader());
- mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), Rect.class);
- mInsets = in.readParcelable(Insets.class.getClassLoader(), Insets.class);
- mTaskId = in.readInt();
- mUserId = in.readInt();
- mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(),
- ComponentName.class);
- } else {
- mBitmapBundle = null;
- mBoundsInScreen = null;
- mInsets = null;
- mTaskId = -1;
- mUserId = -1;
- mTopComponent = null;
- }
- }
-
- @ScreenshotType
- public int getType() {
- return mType;
- }
-
- @ScreenshotSource
- public int getSource() {
- return mSource;
- }
-
- public Bundle getBitmapBundle() {
- return mBitmapBundle;
- }
-
- public Rect getBoundsInScreen() {
- return mBoundsInScreen;
- }
-
- public Insets getInsets() {
- return mInsets;
- }
-
- public int getTaskId() {
- return mTaskId;
- }
-
- public int getUserId() {
- return mUserId;
- }
-
- public ComponentName getTopComponent() {
- return mTopComponent;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mType);
- dest.writeInt(mSource);
- if (mBitmapBundle == null) {
- dest.writeInt(0);
- } else {
- dest.writeInt(1);
- dest.writeBundle(mBitmapBundle);
- dest.writeParcelable(mBoundsInScreen, 0);
- dest.writeParcelable(mInsets, 0);
- dest.writeInt(mTaskId);
- dest.writeInt(mUserId);
- dest.writeParcelable(mTopComponent, 0);
- }
- }
-
- @NonNull
- public static final Parcelable.Creator<ScreenshotRequest> CREATOR =
- new Parcelable.Creator<ScreenshotRequest>() {
-
- @Override
- public ScreenshotRequest createFromParcel(Parcel source) {
- return new ScreenshotRequest(source);
- }
-
- @Override
- public ScreenshotRequest[] newArray(int size) {
- return new ScreenshotRequest[size];
- }
- };
- }
-
- /**
- * Bundler used to convert between a hardware bitmap and a bundle without copying the internal
- * content. This is expected to be used together with {@link #provideScreenshot} to handle a
- * hardware bitmap as a screenshot.
- */
- public static final class HardwareBitmapBundler {
- private static final String KEY_BUFFER = "bitmap_util_buffer";
- private static final String KEY_COLOR_SPACE = "bitmap_util_color_space";
-
- private HardwareBitmapBundler() {
- }
-
- /**
- * Creates a Bundle that represents the given Bitmap.
- * <p>The Bundle will contain a wrapped version of the Bitmaps HardwareBuffer, so will avoid
- * copies when passing across processes, only pass to processes you trust.
- *
- * <p>Returns a new Bundle rather than modifying an exiting one to avoid key collisions, the
- * returned Bundle should be treated as a standalone object.
- *
- * @param bitmap to convert to bundle
- * @return a Bundle representing the bitmap, should only be parsed by
- * {@link #bundleToHardwareBitmap(Bundle)}
- */
- public static Bundle hardwareBitmapToBundle(Bitmap bitmap) {
- if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
- throw new IllegalArgumentException(
- "Passed bitmap must have hardware config, found: " + bitmap.getConfig());
- }
-
- // Bitmap assumes SRGB for null color space
- ParcelableColorSpace colorSpace =
- bitmap.getColorSpace() == null
- ? new ParcelableColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
- : new ParcelableColorSpace(bitmap.getColorSpace());
-
- Bundle bundle = new Bundle();
- bundle.putParcelable(KEY_BUFFER, bitmap.getHardwareBuffer());
- bundle.putParcelable(KEY_COLOR_SPACE, colorSpace);
-
- return bundle;
- }
-
- /**
- * Extracts the Bitmap added to a Bundle with {@link #hardwareBitmapToBundle(Bitmap)} .}
- *
- * <p>This Bitmap contains the HardwareBuffer from the original caller, be careful passing
- * this Bitmap on to any other source.
- *
- * @param bundle containing the bitmap
- * @return a hardware Bitmap
- */
- public static Bitmap bundleToHardwareBitmap(Bundle bundle) {
- if (!bundle.containsKey(KEY_BUFFER) || !bundle.containsKey(KEY_COLOR_SPACE)) {
- throw new IllegalArgumentException("Bundle does not contain a hardware bitmap");
- }
-
- HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER, HardwareBuffer.class);
- ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE,
- ParcelableColorSpace.class);
-
- return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),
- colorSpace.getColorSpace());
- }
- }
-
private static final String TAG = "ScreenshotHelper";
// Time until we give up on the screenshot & show an error instead.
@@ -277,20 +60,35 @@
/**
* Request a screenshot be taken.
* <p>
- * Added to support reducing unit test duration; the method variant without a timeout argument
- * is recommended for general use.
+ * Convenience method for taking a full screenshot with provided source.
*
- * @param type The type of screenshot, defined by {@link ScreenshotType}
- * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
- * @param handler used to process messages received from the screenshot service
+ * @param source source of the screenshot request, defined by {@link
+ * ScreenshotSource}
+ * @param handler used to process messages received from the screenshot service
* @param completionConsumer receives the URI of the captured screenshot, once saved or
- * null if no screenshot was saved
+ * null if no screenshot was saved
*/
- public void takeScreenshot(@ScreenshotType int type, @ScreenshotSource int source,
- @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
- ScreenshotRequest screenshotRequest = new ScreenshotRequest(type, source);
- takeScreenshot(handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS,
- completionConsumer);
+ public void takeScreenshot(@ScreenshotSource int source, @NonNull Handler handler,
+ @Nullable Consumer<Uri> completionConsumer) {
+ ScreenshotRequest request =
+ new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, source).build();
+ takeScreenshot(request, handler, completionConsumer);
+ }
+
+ /**
+ * Request a screenshot be taken.
+ * <p>
+ *
+ * @param request description of the screenshot request, either for taking a
+ * screenshot or
+ * providing a bitmap
+ * @param handler used to process messages received from the screenshot service
+ * @param completionConsumer receives the URI of the captured screenshot, once saved or
+ * null if no screenshot was saved
+ */
+ public void takeScreenshot(ScreenshotRequest request, @NonNull Handler handler,
+ @Nullable Consumer<Uri> completionConsumer) {
+ takeScreenshotInternal(request, handler, completionConsumer, SCREENSHOT_TIMEOUT_MS);
}
/**
@@ -299,46 +97,16 @@
* Added to support reducing unit test duration; the method variant without a timeout argument
* is recommended for general use.
*
- * @param type The type of screenshot, defined by {@link ScreenshotType}
- * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
- * @param handler used to process messages received from the screenshot service
- * @param timeoutMs time limit for processing, intended only for testing
+ * @param request description of the screenshot request, either for taking a
+ * screenshot or providing a bitmap
+ * @param handler used to process messages received from the screenshot service
+ * @param timeoutMs time limit for processing, intended only for testing
* @param completionConsumer receives the URI of the captured screenshot, once saved or
- * null if no screenshot was saved
+ * null if no screenshot was saved
*/
@VisibleForTesting
- public void takeScreenshot(@ScreenshotType int type, @ScreenshotSource int source,
- @NonNull Handler handler, long timeoutMs, @Nullable Consumer<Uri> completionConsumer) {
- ScreenshotRequest screenshotRequest = new ScreenshotRequest(type, source);
- takeScreenshot(handler, screenshotRequest, timeoutMs, completionConsumer);
- }
-
- /**
- * Request that provided image be handled as if it was a screenshot.
- *
- * @param screenshotBundle Bundle containing the buffer and color space of the screenshot.
- * @param boundsInScreen The bounds in screen coordinates that the bitmap originated from.
- * @param insets The insets that the image was shown with, inside the screen bounds.
- * @param taskId The taskId of the task that the screen shot was taken of.
- * @param userId The userId of user running the task provided in taskId.
- * @param topComponent The component name of the top component running in the task.
- * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
- * @param handler A handler used in case the screenshot times out
- * @param completionConsumer receives the URI of the captured screenshot, once saved or
- * null if no screenshot was saved
- */
- public void provideScreenshot(@NonNull Bundle screenshotBundle, @NonNull Rect boundsInScreen,
- @NonNull Insets insets, int taskId, int userId, ComponentName topComponent,
- @ScreenshotSource int source, @NonNull Handler handler,
- @Nullable Consumer<Uri> completionConsumer) {
- ScreenshotRequest screenshotRequest = new ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE,
- source, screenshotBundle, boundsInScreen, insets, taskId, userId, topComponent);
- takeScreenshot(handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS, completionConsumer);
- }
-
- private void takeScreenshot(@NonNull Handler handler,
- ScreenshotRequest screenshotRequest, long timeoutMs,
- @Nullable Consumer<Uri> completionConsumer) {
+ public void takeScreenshotInternal(ScreenshotRequest request, @NonNull Handler handler,
+ @Nullable Consumer<Uri> completionConsumer, long timeoutMs) {
synchronized (mScreenshotLock) {
final Runnable mScreenshotTimeout = () -> {
@@ -354,7 +122,7 @@
}
};
- Message msg = Message.obtain(null, 0, screenshotRequest);
+ Message msg = Message.obtain(null, 0, request);
Handler h = new Handler(handler.getLooper()) {
@Override
@@ -471,5 +239,4 @@
Intent.FLAG_RECEIVER_FOREGROUND);
mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
}
-
}
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.aidl b/core/java/com/android/internal/util/ScreenshotRequest.aidl
new file mode 100644
index 0000000..b08905d
--- /dev/null
+++ b/core/java/com/android/internal/util/ScreenshotRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+parcelable ScreenshotRequest;
\ No newline at end of file
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
new file mode 100644
index 0000000..1902f80
--- /dev/null
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -0,0 +1,332 @@
+/*
+ * 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.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.UserHandle.USER_NULL;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.Insets;
+import android.graphics.ParcelableColorSpace;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.WindowManager;
+
+import java.util.Objects;
+
+/**
+ * Describes a screenshot request.
+ */
+public class ScreenshotRequest implements Parcelable {
+ private static final String TAG = "ScreenshotRequest";
+
+ @WindowManager.ScreenshotType
+ private final int mType;
+ @WindowManager.ScreenshotSource
+ private final int mSource;
+ private final ComponentName mTopComponent;
+ private final int mTaskId;
+ private final int mUserId;
+ private final Bitmap mBitmap;
+ private final Rect mBoundsInScreen;
+ private final Insets mInsets;
+
+ private ScreenshotRequest(
+ @WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
+ ComponentName topComponent, int taskId, int userId,
+ Bitmap bitmap, Rect boundsInScreen, Insets insets) {
+ mType = type;
+ mSource = source;
+ mTopComponent = topComponent;
+ mTaskId = taskId;
+ mUserId = userId;
+ mBitmap = bitmap;
+ mBoundsInScreen = boundsInScreen;
+ mInsets = insets;
+ }
+
+ ScreenshotRequest(Parcel in) {
+ mType = in.readInt();
+ mSource = in.readInt();
+ mTopComponent = in.readTypedObject(ComponentName.CREATOR);
+ mTaskId = in.readInt();
+ mUserId = in.readInt();
+ mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
+ mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
+ mInsets = in.readTypedObject(Insets.CREATOR);
+ }
+
+ @WindowManager.ScreenshotType
+ public int getType() {
+ return mType;
+ }
+
+ @WindowManager.ScreenshotSource
+ public int getSource() {
+ return mSource;
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ public Rect getBoundsInScreen() {
+ return mBoundsInScreen;
+ }
+
+ public Insets getInsets() {
+ return mInsets;
+ }
+
+ public int getTaskId() {
+ return mTaskId;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public ComponentName getTopComponent() {
+ return mTopComponent;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mSource);
+ dest.writeTypedObject(mTopComponent, 0);
+ dest.writeInt(mTaskId);
+ dest.writeInt(mUserId);
+ dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
+ dest.writeTypedObject(mBoundsInScreen, 0);
+ dest.writeTypedObject(mInsets, 0);
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<ScreenshotRequest> CREATOR =
+ new Parcelable.Creator<ScreenshotRequest>() {
+
+ @Override
+ public ScreenshotRequest createFromParcel(Parcel source) {
+ return new ScreenshotRequest(source);
+ }
+
+ @Override
+ public ScreenshotRequest[] newArray(int size) {
+ return new ScreenshotRequest[size];
+ }
+ };
+
+ /**
+ * Builder class for {@link ScreenshotRequest} objects.
+ */
+ public static class Builder {
+ @WindowManager.ScreenshotType
+ private final int mType;
+
+ @WindowManager.ScreenshotSource
+ private final int mSource;
+
+ private Bitmap mBitmap;
+ private Rect mBoundsInScreen;
+ private Insets mInsets = Insets.NONE;
+ private int mTaskId = INVALID_TASK_ID;
+ private int mUserId = USER_NULL;
+ private ComponentName mTopComponent;
+
+ /**
+ * Begin building a ScreenshotRequest.
+ *
+ * @param type The type of the screenshot request, defined by {@link
+ * WindowManager.ScreenshotType}
+ * @param source The source of the screenshot request, defined by {@link
+ * WindowManager.ScreenshotSource}
+ */
+ public Builder(
+ @WindowManager.ScreenshotType int type,
+ @WindowManager.ScreenshotSource int source) {
+ mType = type;
+ mSource = source;
+ }
+
+ /**
+ * Construct a new {@link ScreenshotRequest} with the set parameters.
+ */
+ public ScreenshotRequest build() {
+ if (mType == TAKE_SCREENSHOT_FULLSCREEN && mBitmap != null) {
+ Log.w(TAG, "Bitmap provided, but request is fullscreen. Bitmap will be ignored.");
+ }
+ if (mType == TAKE_SCREENSHOT_PROVIDED_IMAGE && mBitmap == null) {
+ throw new IllegalStateException(
+ "Request is PROVIDED_IMAGE, but no bitmap is provided!");
+ }
+
+ return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
+ mBoundsInScreen, mInsets);
+ }
+
+ /**
+ * Set the top component associated with this request.
+ *
+ * @param topComponent The component name of the top component running in the task.
+ */
+ public Builder setTopComponent(ComponentName topComponent) {
+ mTopComponent = topComponent;
+ return this;
+ }
+
+ /**
+ * Set the task id associated with this request.
+ *
+ * @param taskId The taskId of the task that the screenshot was taken of.
+ */
+ public Builder setTaskId(int taskId) {
+ mTaskId = taskId;
+ return this;
+ }
+
+ /**
+ * Set the user id associated with this request.
+ *
+ * @param userId The userId of user running the task provided in taskId.
+ */
+ public Builder setUserId(int userId) {
+ mUserId = userId;
+ return this;
+ }
+
+ /**
+ * Set the bitmap associated with this request.
+ *
+ * @param bitmap The provided screenshot.
+ */
+ public Builder setBitmap(Bitmap bitmap) {
+ mBitmap = bitmap;
+ return this;
+ }
+
+ /**
+ * Set the bounds for the provided bitmap.
+ *
+ * @param bounds The bounds in screen coordinates that the bitmap originated from.
+ */
+ public Builder setBoundsOnScreen(Rect bounds) {
+ mBoundsInScreen = bounds;
+ return this;
+ }
+
+ /**
+ * Set the insets for the provided bitmap.
+ *
+ * @param insets The insets that the image was shown with, inside the screen bounds.
+ */
+ public Builder setInsets(@NonNull Insets insets) {
+ mInsets = insets;
+ return this;
+ }
+ }
+
+ /**
+ * Bundler used to convert between a hardware bitmap and a bundle without copying the internal
+ * content. This is used together with a fully-defined ScreenshotRequest to handle a hardware
+ * bitmap as a screenshot.
+ */
+ private static final class HardwareBitmapBundler {
+ private static final String KEY_BUFFER = "bitmap_util_buffer";
+ private static final String KEY_COLOR_SPACE = "bitmap_util_color_space";
+
+ private HardwareBitmapBundler() {
+ }
+
+ /**
+ * Creates a Bundle that represents the given Bitmap.
+ * <p>The Bundle will contain a wrapped version of the Bitmaps HardwareBuffer, so will
+ * avoid
+ * copies when passing across processes, only pass to processes you trust.
+ *
+ * <p>Returns a new Bundle rather than modifying an exiting one to avoid key collisions,
+ * the
+ * returned Bundle should be treated as a standalone object.
+ *
+ * @param bitmap to convert to bundle
+ * @return a Bundle representing the bitmap, should only be parsed by
+ * {@link #bundleToHardwareBitmap(Bundle)}
+ */
+ private static Bundle hardwareBitmapToBundle(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ if (bitmap.getConfig() != Bitmap.Config.HARDWARE) {
+ throw new IllegalArgumentException(
+ "Passed bitmap must have hardware config, found: "
+ + bitmap.getConfig());
+ }
+
+ // Bitmap assumes SRGB for null color space
+ ParcelableColorSpace colorSpace =
+ bitmap.getColorSpace() == null
+ ? new ParcelableColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
+ : new ParcelableColorSpace(bitmap.getColorSpace());
+
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(KEY_BUFFER, bitmap.getHardwareBuffer());
+ bundle.putParcelable(KEY_COLOR_SPACE, colorSpace);
+
+ return bundle;
+ }
+
+ /**
+ * Extracts the Bitmap added to a Bundle with {@link #hardwareBitmapToBundle(Bitmap)}.
+ *
+ * <p>This Bitmap contains the HardwareBuffer from the original caller, be careful
+ * passing
+ * this Bitmap on to any other source.
+ *
+ * @param bundle containing the bitmap
+ * @return a hardware Bitmap
+ */
+ private static Bitmap bundleToHardwareBitmap(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ if (!bundle.containsKey(KEY_BUFFER) || !bundle.containsKey(KEY_COLOR_SPACE)) {
+ throw new IllegalArgumentException("Bundle does not contain a hardware bitmap");
+ }
+
+ HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER, HardwareBuffer.class);
+ ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE,
+ ParcelableColorSpace.class);
+
+ return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),
+ colorSpace.getColorSpace());
+ }
+ }
+}
diff --git a/core/java/com/android/internal/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java
index 869da1f..058c6ec 100644
--- a/core/java/com/android/internal/view/RotationPolicy.java
+++ b/core/java/com/android/internal/view/RotationPolicy.java
@@ -106,7 +106,9 @@
* Enables or disables rotation lock from the system UI toggle.
*/
public static void setRotationLock(Context context, final boolean enabled) {
- final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION;
+ final int rotation = areAllRotationsAllowed(context)
+ || useCurrentRotationOnRotationLockChange(context) ? CURRENT_ROTATION
+ : NATURAL_ROTATION;
setRotationLockAtAngle(context, enabled, rotation);
}
@@ -139,6 +141,11 @@
return context.getResources().getBoolean(R.bool.config_allowAllRotations);
}
+ private static boolean useCurrentRotationOnRotationLockChange(Context context) {
+ return context.getResources().getBoolean(
+ R.bool.config_useCurrentRotationOnRotationLockChange);
+ }
+
private static void setRotationLock(final boolean enabled, final int rotation) {
AsyncTask.execute(new Runnable() {
@Override
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index 8e4006a..e029af4 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -110,11 +110,20 @@
// All of this type/caption enabled for current profiles.
repeated android.content.ComponentNameProto enabled = 3;
-
repeated ManagedServiceInfoProto live_services = 4;
+ // Was: repeated ComponentNameProto, when snoozed services were not per-user-id.
+ reserved 5;
+
+ message SnoozedServices {
+ option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
+ optional int32 user_id = 1;
+ repeated android.content.ComponentNameProto snoozed = 2;
+ }
+
// Snoozed for current profiles.
- repeated android.content.ComponentNameProto snoozed = 5;
+ repeated SnoozedServices snoozed = 6;
}
message RankingHelperProto {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7002a52..b2526dd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -315,6 +315,7 @@
<protected-broadcast android:name="android.media.MASTER_BALANCE_CHANGED_ACTION" />
<protected-broadcast android:name="android.media.SCO_AUDIO_STATE_CHANGED" />
<protected-broadcast android:name="android.media.ACTION_SCO_AUDIO_STATE_UPDATED" />
+ <protected-broadcast android:name="com.android.server.audio.action.CHECK_MUSIC_ACTIVE" />
<protected-broadcast android:name="android.intent.action.MEDIA_REMOVED" />
<protected-broadcast android:name="android.intent.action.MEDIA_UNMOUNTED" />
@@ -3884,7 +3885,7 @@
<p>Should only be requested by the System, should be required by
TileService declarations.-->
<permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE"
- android:protectionLevel="signature" />
+ android:protectionLevel="signature|recents" />
<!-- Allows SystemUI to request third party controls.
<p>Should only be requested by the System and required by
diff --git a/core/res/res/layout/autofill_fill_dialog.xml b/core/res/res/layout/autofill_fill_dialog.xml
index c382a65..2e65800 100644
--- a/core/res/res/layout/autofill_fill_dialog.xml
+++ b/core/res/res/layout/autofill_fill_dialog.xml
@@ -93,7 +93,7 @@
android:layout_height="36dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
- style="@style/AutofillHalfSheetOutlinedButton"
+ style="?android:attr/borderlessButtonStyle"
android:text="@string/autofill_save_no">
</Button>
diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml
index fd08241..3c0b789 100644
--- a/core/res/res/layout/autofill_save.xml
+++ b/core/res/res/layout/autofill_save.xml
@@ -81,7 +81,7 @@
android:layout_height="36dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
- style="@style/AutofillHalfSheetOutlinedButton"
+ style="?android:attr/borderlessButtonStyle"
android:text="@string/autofill_save_no">
</Button>
diff --git a/core/res/res/layout/resolve_grid_item.xml b/core/res/res/layout/resolve_grid_item.xml
index 50e6f33..a5ff470 100644
--- a/core/res/res/layout/resolve_grid_item.xml
+++ b/core/res/res/layout/resolve_grid_item.xml
@@ -17,6 +17,7 @@
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/item"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/core/res/res/values/bools.xml b/core/res/res/values/bools.xml
index 4b27bf2..fe296c7 100644
--- a/core/res/res/values/bools.xml
+++ b/core/res/res/values/bools.xml
@@ -18,7 +18,6 @@
<bool name="kg_enable_camera_default_widget">true</bool>
<bool name="kg_center_small_widgets_vertically">false</bool>
<bool name="kg_top_align_page_shrink_on_bouncer_visible">true</bool>
- <bool name="kg_wake_on_acquire_start">false</bool>
<bool name="action_bar_embed_tabs">true</bool>
<bool name="split_action_bar_is_narrow">true</bool>
<bool name="preferences_prefer_dual_pane">false</bool>
diff --git a/core/res/res/values/colors_material.xml b/core/res/res/values/colors_material.xml
index ea6e1f1..a99ba15 100644
--- a/core/res/res/values/colors_material.xml
+++ b/core/res/res/values/colors_material.xml
@@ -72,8 +72,8 @@
<item name="secondary_content_alpha_material_dark" format="float" type="dimen">.7</item>
<item name="secondary_content_alpha_material_light" format="float" type="dimen">0.60</item>
- <item name="highlight_alpha_material_light" format="float" type="dimen">0.10</item>
- <item name="highlight_alpha_material_dark" format="float" type="dimen">0.10</item>
+ <item name="highlight_alpha_material_light" format="float" type="dimen">0.5</item>
+ <item name="highlight_alpha_material_dark" format="float" type="dimen">0.5</item>
<item name="highlight_alpha_material_colored" format="float" type="dimen">0.10</item>
<!-- Primary & accent colors -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index cdfde3e..104872e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -560,6 +560,10 @@
rotations as the default behavior. -->
<bool name="config_allowAllRotations">false</bool>
+ <!-- If false and config_allowAllRotations is false, the screen will rotate to the natural
+ orientation of the device when the auto-rotate policy is toggled. -->
+ <bool name="config_useCurrentRotationOnRotationLockChange">false</bool>
+
<!-- If true, the direction rotation is applied to get to an application's requested
orientation is reversed. Normally, the model is that landscape is
clockwise from portrait; thus on a portrait device an app requesting
@@ -632,6 +636,16 @@
The default is false. -->
<bool name="config_lidControlsSleep">false</bool>
+ <!-- The device states (supplied by DeviceStateManager) that should be treated as open by the
+ device fold controller. Default is empty. -->
+ <integer-array name="config_openDeviceStates">
+ <!-- Example:
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ -->
+ </integer-array>
+
<!-- The device states (supplied by DeviceStateManager) that should be treated as folded by the
display fold controller. Default is empty. -->
<integer-array name="config_foldedDeviceStates">
@@ -652,6 +666,16 @@
-->
</integer-array>
+ <!-- The device states (supplied by DeviceStateManager) that should be treated as a rear display
+ state. Default is empty. -->
+ <integer-array name="config_rearDisplayDeviceStates">
+ <!-- Example:
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ -->
+ </integer-array>
+
<!-- Indicates whether the window manager reacts to half-fold device states by overriding
rotation. -->
<bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool>
@@ -944,6 +968,15 @@
<!-- Boolean indicating whether light mode is allowed when DWB is turned on. -->
<bool name="config_displayWhiteBalanceLightModeAllowed">true</bool>
+ <!-- Duration, in milliseconds, of the display white balance animated transitions. -->
+ <integer name="config_displayWhiteBalanceTransitionTime">3000</integer>
+
+ <!-- Device states where the sensor based rotation values should be reversed around the Z axis
+ for the default display.
+ TODO(b/265312193): Remove this workaround when this bug is fixed.-->
+ <integer-array name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis">
+ </integer-array>
+
<!-- Indicate available ColorDisplayManager.COLOR_MODE_xxx. -->
<integer-array name="config_availableColorModes">
<!-- Example:
@@ -2015,6 +2048,9 @@
STREAM_MUSIC as if it's on TV platform. -->
<bool name="config_single_volume">false</bool>
+ <!-- Volume policy -->
+ <bool name="config_volume_down_to_enter_silent">false</bool>
+
<!-- The number of volume steps for the notification stream -->
<integer name="config_audio_notif_vol_steps">7</integer>
@@ -4919,9 +4955,8 @@
<!-- If face auth sends the user directly to home/last open app, or stays on keyguard -->
<bool name="config_faceAuthDismissesKeyguard">true</bool>
- <!-- Default value for whether a SFPS device is required to be interactive for fingerprint auth
- to unlock the device. -->
- <bool name="config_requireScreenOnToAuthEnabled">false</bool>
+ <!-- Default value for performant auth feature. -->
+ <bool name="config_performantAuthDefault">false</bool>
<!-- The component name for the default profile supervisor, which can be set as a profile owner
even after user setup is complete. The defined component should be used for supervision purposes
@@ -5300,6 +5335,9 @@
<!-- Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. -->
<bool name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled">false</bool>
+ <!-- Whether using display aspect ratio as a default aspect ratio for all letterboxed apps. -->
+ <bool name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled">false</bool>
+
<!-- Whether the specific behaviour for translucent activities letterboxing is enabled.
TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
<bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 4d2aeaa..d16ccaa 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5142,6 +5142,14 @@
<!-- Cling help message confirmation button when hiding the navigation bar entering immersive mode [CHAR LIMIT=30] -->
<string name="immersive_cling_positive">Got it</string>
+ <!-- Text on a toast shown after the system rotates the screen for camera app
+ compatibility. [CHAR LIMIT=NONE] -->
+ <string name="display_rotation_camera_compat_toast_after_rotation">Rotate for a better view</string>
+
+ <!-- Text on a toast shown when a camera view is started within the app that may not be able
+ to display the camera preview correctly while in split screen. [CHAR LIMIT=NONE] -->
+ <string name="display_rotation_camera_compat_toast_in_split_screen">Exit split screen for a better view</string>
+
<!-- Label for button to confirm chosen date or time [CHAR LIMIT=30] -->
<string name="done_label">Done</string>
<!--
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3b9fcbe..408203c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -321,6 +321,7 @@
<java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_kazakhstan" />
<java-symbol type="integer" name="config_phonenumber_compare_min_match" />
<java-symbol type="bool" name="config_single_volume" />
+ <java-symbol type="bool" name="config_volume_down_to_enter_silent" />
<java-symbol type="bool" name="config_voice_capable" />
<java-symbol type="bool" name="config_requireCallCapableAccountForHandle" />
<java-symbol type="bool" name="config_user_notification_of_restrictied_mobile_access" />
@@ -1721,6 +1722,7 @@
<java-symbol type="attr" name="dialogTitleDecorLayout" />
<java-symbol type="attr" name="dialogTitleIconsDecorLayout" />
<java-symbol type="bool" name="config_allowAllRotations" />
+ <java-symbol type="bool" name="config_useCurrentRotationOnRotationLockChange"/>
<java-symbol type="bool" name="config_annoy_dianne" />
<java-symbol type="bool" name="config_startDreamImmediatelyOnDock" />
<java-symbol type="bool" name="config_carDockEnablesAccelerometer" />
@@ -2712,7 +2714,7 @@
<java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" />
<java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" />
<java-symbol type="bool" name="config_faceAuthDismissesKeyguard" />
- <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" />
+ <java-symbol type="bool" name="config_performantAuthDefault" />
<!-- Face config -->
<java-symbol type="integer" name="config_faceMaxTemplatesPerUser" />
@@ -2827,7 +2829,6 @@
<java-symbol type="dimen" name="fast_scroller_minimum_touch_target" />
<java-symbol type="array" name="config_cdma_international_roaming_indicators" />
<java-symbol type="string" name="kg_text_message_separator" />
- <java-symbol type="bool" name="kg_wake_on_acquire_start" />
<java-symbol type="bool" name="config_use_sim_language_file" />
<java-symbol type="bool" name="config_LTE_eri_for_network_name" />
@@ -3424,6 +3425,12 @@
<java-symbol type="array" name="config_displayWhiteBalanceDisplayPrimaries" />
<java-symbol type="array" name="config_displayWhiteBalanceDisplayNominalWhite" />
<java-symbol type="bool" name="config_displayWhiteBalanceLightModeAllowed" />
+ <java-symbol type="integer" name="config_displayWhiteBalanceTransitionTime" />
+
+ <!-- Device states where the sensor based rotation values should be reversed around the Z axis
+ for the default display.
+ TODO(b/265312193): Remove this workaround when this bug is fixed.-->
+ <java-symbol type="array" name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis" />
<!-- Default first user restrictions -->
<java-symbol type="array" name="config_defaultFirstUserRestrictions" />
@@ -4003,8 +4010,10 @@
<java-symbol type="integer" name="config_maxScanTasksForHomeVisibility" />
<!-- For Foldables -->
+ <java-symbol type="array" name="config_openDeviceStates" />
<java-symbol type="array" name="config_foldedDeviceStates" />
<java-symbol type="array" name="config_halfFoldedDeviceStates" />
+ <java-symbol type="array" name="config_rearDisplayDeviceStates" />
<java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" />
<java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" />
<java-symbol type="array" name="config_deviceStatesOnWhichToSleep" />
@@ -4486,6 +4495,7 @@
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
+ <java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" />
<java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
<java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index ed2b101..3768063 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -368,4 +368,20 @@
PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
assertEquals(n1, "cache_key.bluetooth.get_state");
}
+
+ @Test
+ public void testOnTrimMemory() {
+ TestCache cache = new TestCache(MODULE, "trimMemoryTest");
+ // The cache is not active until it has been invalidated once.
+ cache.invalidateCache();
+ // Populate the cache with six entries.
+ for (int i = 0; i < 6; i++) {
+ cache.query(i);
+ }
+ // The maximum number of entries in TestCache is 4, so even though six entries were
+ // created, only four are retained.
+ assertEquals(4, cache.size());
+ PropertyInvalidatedCache.onTrimMemory();
+ assertEquals(0, cache.size());
+ }
}
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index f370ebd..9d6b29e 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -17,6 +17,7 @@
package android.window;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
@@ -60,8 +61,8 @@
private OnBackAnimationCallback mCallback1;
@Mock
private OnBackAnimationCallback mCallback2;
- @Mock
- private BackEvent mBackEvent;
+ private final BackMotionEvent mBackEvent = new BackMotionEvent(
+ 0, 0, 0, BackEvent.EDGE_LEFT, null);
@Before
public void setUp() throws Exception {
@@ -89,12 +90,12 @@
captor.capture());
captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent);
waitForIdle();
- verify(mCallback1).onBackStarted(mBackEvent);
+ verify(mCallback1).onBackStarted(any(BackEvent.class));
verifyZeroInteractions(mCallback2);
captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent);
waitForIdle();
- verify(mCallback2).onBackStarted(mBackEvent);
+ verify(mCallback2).onBackStarted(any(BackEvent.class));
verifyNoMoreInteractions(mCallback1);
}
@@ -114,7 +115,7 @@
assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY);
captor.getValue().getCallback().onBackStarted(mBackEvent);
waitForIdle();
- verify(mCallback1).onBackStarted(mBackEvent);
+ verify(mCallback1).onBackStarted(any(BackEvent.class));
}
@Test
@@ -152,6 +153,6 @@
verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture());
captor.getValue().getCallback().onBackStarted(mBackEvent);
waitForIdle();
- verify(mCallback2).onBackStarted(mBackEvent);
+ verify(mCallback2).onBackStarted(any(BackEvent.class));
}
}
diff --git a/core/tests/screenshothelpertests/Android.bp b/core/tests/screenshothelpertests/Android.bp
index 37af99c..3c71e6e 100644
--- a/core/tests/screenshothelpertests/Android.bp
+++ b/core/tests/screenshothelpertests/Android.bp
@@ -13,7 +13,7 @@
srcs: [
"src/**/*.java",
],
-
+
static_libs: [
"frameworks-base-testutils",
"androidx.test.runner",
@@ -21,6 +21,7 @@
"androidx.test.ext.junit",
"mockito-target-minus-junit4",
"platform-test-annotations",
+ "testng",
],
libs: [
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
index 2719431..5c9894e 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
@@ -17,6 +17,7 @@
package com.android.internal.util;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;
@@ -31,9 +32,11 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
import android.graphics.Insets;
import android.graphics.Rect;
-import android.os.Bundle;
+import android.hardware.HardwareBuffer;
import android.os.Handler;
import android.os.Looper;
import android.view.WindowManager;
@@ -79,30 +82,48 @@
@Test
public void testFullscreenScreenshot() {
- mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
+ mScreenshotHelper.takeScreenshot(
WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
}
@Test
+ public void testFullscreenScreenshotRequest() {
+ ScreenshotRequest request = new ScreenshotRequest.Builder(
+ TAKE_SCREENSHOT_FULLSCREEN, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
+ .build();
+ mScreenshotHelper.takeScreenshot(request, mHandler, null);
+ }
+
+ @Test
public void testProvidedImageScreenshot() {
- mScreenshotHelper.provideScreenshot(
- new Bundle(), new Rect(), Insets.of(0, 0, 0, 0), 1, 1, new ComponentName("", ""),
- WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
+ HardwareBuffer buffer = HardwareBuffer.create(
+ 10, 10, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
+ ScreenshotRequest request = new ScreenshotRequest.Builder(
+ TAKE_SCREENSHOT_PROVIDED_IMAGE, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
+ .setTopComponent(new ComponentName("", ""))
+ .setTaskId(1)
+ .setUserId(1)
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(new Rect())
+ .setInsets(Insets.NONE)
+ .build();
+ mScreenshotHelper.takeScreenshot(request, mHandler, null);
}
@Test
public void testScreenshotTimesOut() {
long timeoutMs = 10;
+ ScreenshotRequest request = new ScreenshotRequest.Builder(
+ TAKE_SCREENSHOT_FULLSCREEN, WindowManager.ScreenshotSource.SCREENSHOT_OTHER)
+ .build();
CountDownLatch lock = new CountDownLatch(1);
- mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
- WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
- mHandler,
- timeoutMs,
+ mScreenshotHelper.takeScreenshotInternal(request, mHandler,
uri -> {
assertNull(uri);
lock.countDown();
- });
+ }, timeoutMs);
try {
// Add tolerance for delay to prevent flakes.
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
new file mode 100644
index 0000000..30540a5
--- /dev/null
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.UserHandle.USER_NULL;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class ScreenshotRequestTest {
+ private final ComponentName mComponentName =
+ new ComponentName("android.test", "android.test.Component");
+
+ @Test
+ public void testSimpleScreenshot() {
+ ScreenshotRequest in =
+ new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build();
+
+ Parcel parcel = Parcel.obtain();
+ in.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
+
+ assertEquals(TAKE_SCREENSHOT_FULLSCREEN, out.getType());
+ assertEquals(SCREENSHOT_OTHER, out.getSource());
+ assertNull("Top component was expected to be null", out.getTopComponent());
+ assertEquals(INVALID_TASK_ID, out.getTaskId());
+ assertEquals(USER_NULL, out.getUserId());
+ assertNull("Bitmap was expected to be null", out.getBitmap());
+ assertNull("Bounds were expected to be null", out.getBoundsInScreen());
+ assertEquals(Insets.NONE, out.getInsets());
+ }
+
+ @Test
+ public void testProvidedScreenshot() {
+ Bitmap bitmap = makeHardwareBitmap(50, 50);
+ ScreenshotRequest in =
+ new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+ .setTopComponent(mComponentName)
+ .setTaskId(2)
+ .setUserId(3)
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(new Rect(10, 10, 60, 60))
+ .setInsets(Insets.of(2, 3, 4, 5))
+ .build();
+
+ Parcel parcel = Parcel.obtain();
+ in.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
+
+ assertEquals(TAKE_SCREENSHOT_PROVIDED_IMAGE, out.getType());
+ assertEquals(SCREENSHOT_OTHER, out.getSource());
+ assertEquals(mComponentName, out.getTopComponent());
+ assertEquals(2, out.getTaskId());
+ assertEquals(3, out.getUserId());
+ assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
+ assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
+ assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
+ }
+
+ @Test
+ public void testProvidedScreenshot_nullBitmap() {
+ ScreenshotRequest.Builder inBuilder =
+ new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+ .setTopComponent(mComponentName)
+ .setTaskId(2)
+ .setUserId(3)
+ .setBoundsOnScreen(new Rect(10, 10, 60, 60))
+ .setInsets(Insets.of(2, 3, 4, 5));
+
+ assertThrows(IllegalStateException.class, inBuilder::build);
+ }
+
+ @Test
+ public void testFullScreenshot_withBitmap() {
+ // A bitmap added to a FULLSCREEN request will be ignored, but it's technically valid
+ Bitmap bitmap = makeHardwareBitmap(50, 50);
+ ScreenshotRequest in =
+ new ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
+ .setBitmap(bitmap)
+ .build();
+
+ Parcel parcel = Parcel.obtain();
+ in.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ScreenshotRequest out = ScreenshotRequest.CREATOR.createFromParcel(parcel);
+
+ assertEquals(TAKE_SCREENSHOT_FULLSCREEN, out.getType());
+ assertEquals(SCREENSHOT_OTHER, out.getSource());
+ assertNull(out.getTopComponent());
+ assertEquals(INVALID_TASK_ID, out.getTaskId());
+ assertEquals(USER_NULL, out.getUserId());
+ assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
+ assertNull("Bounds expected to be null", out.getBoundsInScreen());
+ assertEquals(Insets.NONE, out.getInsets());
+ }
+
+ private Bitmap makeHardwareBitmap(int width, int height) {
+ HardwareBuffer buffer = HardwareBuffer.create(
+ width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+ return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
+ }
+}
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f47d9c6..1cf819a 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3331,6 +3331,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "1015746067": {
+ "message": "Display id=%d is ignoring orientation request for %d, return %d following a per-app override for %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayContent.java"
+ },
"1022095595": {
"message": "TaskFragment info changed name=%s",
"level": "VERBOSE",
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index 14dc6a2..6b1cf8b 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -23,7 +23,6 @@
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.ValueAnimator;
-import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -983,9 +982,9 @@
RippleShader shader = new RippleShader();
// Grab the color for the current state and cut the alpha channel in
// half so that the ripple and background together yield full alpha.
- final int color = clampAlpha(mMaskColorFilter == null
+ final int color = mMaskColorFilter == null
? mState.mColor.getColorForState(getState(), Color.BLACK)
- : mMaskColorFilter.getColor());
+ : mMaskColorFilter.getColor();
final int effectColor = mState.mEffectColor.getColorForState(getState(), Color.MAGENTA);
final float noisePhase = AnimationUtils.currentAnimationTimeMillis();
shader.setColor(color, effectColor);
@@ -1008,13 +1007,6 @@
return properties;
}
- private int clampAlpha(@ColorInt int color) {
- if (Color.alpha(color) < 128) {
- return (color & 0x00FFFFFF) | 0x80000000;
- }
- return color;
- }
-
@Override
public void invalidateSelf() {
invalidateSelf(true);
@@ -1229,7 +1221,7 @@
// Grab the color for the current state and cut the alpha channel in
// half so that the ripple and background together yield full alpha.
- final int color = clampAlpha(mState.mColor.getColorForState(getState(), Color.BLACK));
+ final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
final Paint p = mRipplePaint;
if (mMaskColorFilter != null) {
diff --git a/libs/WindowManager/Jetpack/Android.bp b/libs/WindowManager/Jetpack/Android.bp
index dc4b563..a5b192c 100644
--- a/libs/WindowManager/Jetpack/Android.bp
+++ b/libs/WindowManager/Jetpack/Android.bp
@@ -63,6 +63,12 @@
sdk_version: "current",
}
+android_library_import {
+ name: "window-extensions-core",
+ aars: ["window-extensions-core-release.aar"],
+ sdk_version: "current",
+}
+
java_library {
name: "androidx.window.extensions",
srcs: [
@@ -70,7 +76,10 @@
"src/androidx/window/util/**/*.java",
"src/androidx/window/common/**/*.java",
],
- static_libs: ["window-extensions"],
+ static_libs: [
+ "window-extensions",
+ "window-extensions-core",
+ ],
installable: true,
sdk_version: "core_platform",
system_ext_specific: true,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 54edd9e..666b472 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -48,7 +48,7 @@
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return 1;
+ return 2;
}
@NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 87fa63d..00e13c9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -191,10 +191,25 @@
*/
void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
@NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
+ createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
+ null /* pairedActivityToken */);
+ }
+
+ /**
+ * @param ownerToken The token of the activity that creates this task fragment. It does not
+ * have to be a child of this task fragment, but must belong to the same task.
+ * @param pairedActivityToken The token of the activity that will be reparented to this task
+ * fragment. When it is not {@code null}, the task fragment will be
+ * positioned right above it.
+ */
+ void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+ @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode,
+ @Nullable IBinder pairedActivityToken) {
final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
getOrganizerToken(), fragmentToken, ownerToken)
.setInitialBounds(bounds)
.setWindowingMode(windowingMode)
+ .setPairedActivityToken(pairedActivityToken)
.build();
createTaskFragment(wct, fragmentOptions);
}
@@ -216,8 +231,10 @@
private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
@WindowingMode int windowingMode, @NonNull Activity activity) {
- createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
- wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
+ final IBinder reparentActivityToken = activity.getActivityToken();
+ createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode,
+ reparentActivityToken);
+ wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
}
void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 1cd3ea5..8b3a471 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -77,6 +77,9 @@
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.extensions.WindowExtensionsImpl;
+import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.core.util.function.Function;
import androidx.window.extensions.embedding.TransactionManager.TransactionRecord;
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
@@ -87,7 +90,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Main controller class that manages split states and presentation.
@@ -113,7 +115,7 @@
/**
* A developer-defined {@link SplitAttributes} calculator to compute the current
* {@link SplitAttributes} with the current device and window states.
- * It is registered via {@link #setSplitAttributesCalculator(SplitAttributesCalculator)}
+ * It is registered via {@link #setSplitAttributesCalculator(Function)}
* and unregistered via {@link #clearSplitAttributesCalculator()}.
* This is called when:
* <ul>
@@ -126,7 +128,7 @@
*/
@GuardedBy("mLock")
@Nullable
- private SplitAttributesCalculator mSplitAttributesCalculator;
+ private Function<SplitAttributesCalculatorParams, SplitAttributes> mSplitAttributesCalculator;
/**
* Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info
@@ -139,6 +141,7 @@
final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();
/** Callback to Jetpack to notify about changes to split states. */
+ @GuardedBy("mLock")
@Nullable
private Consumer<List<SplitInfo>> mEmbeddingCallback;
private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
@@ -164,7 +167,8 @@
foldingFeatureProducer.addDataChangedCallback(new FoldingFeatureListener());
}
- private class FoldingFeatureListener implements Consumer<List<CommonFoldingFeature>> {
+ private class FoldingFeatureListener
+ implements java.util.function.Consumer<List<CommonFoldingFeature>> {
@Override
public void accept(List<CommonFoldingFeature> foldingFeatures) {
synchronized (mLock) {
@@ -205,7 +209,8 @@
}
@Override
- public void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator) {
+ public void setSplitAttributesCalculator(
+ @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator) {
synchronized (mLock) {
mSplitAttributesCalculator = calculator;
}
@@ -220,7 +225,7 @@
@GuardedBy("mLock")
@Nullable
- SplitAttributesCalculator getSplitAttributesCalculator() {
+ Function<SplitAttributesCalculatorParams, SplitAttributes> getSplitAttributesCalculator() {
return mSplitAttributesCalculator;
}
@@ -233,9 +238,22 @@
/**
* Registers the split organizer callback to notify about changes to active splits.
+ * @deprecated Use {@link #setSplitInfoCallback(Consumer)} starting with
+ * {@link WindowExtensionsImpl#getVendorApiLevel()} 2.
*/
+ @Deprecated
@Override
- public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) {
+ public void setSplitInfoCallback(
+ @NonNull java.util.function.Consumer<List<SplitInfo>> callback) {
+ Consumer<List<SplitInfo>> oemConsumer = callback::accept;
+ setSplitInfoCallback(oemConsumer);
+ }
+
+ /**
+ * Registers the split organizer callback to notify about changes to active splits.
+ * @since {@link WindowExtensionsImpl#getVendorApiLevel()} 2
+ */
+ public void setSplitInfoCallback(Consumer<List<SplitInfo>> callback) {
synchronized (mLock) {
mEmbeddingCallback = callback;
updateCallbackIfNecessary();
@@ -1481,7 +1499,7 @@
* Returns the active split that has the provided containers as primary and secondary or as
* secondary and primary, if available.
*/
- @VisibleForTesting
+ @GuardedBy("mLock")
@Nullable
SplitContainer getActiveSplitForContainers(
@NonNull TaskFragmentContainer firstContainer,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 14d244b..668a7d5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -43,11 +43,11 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Function;
import androidx.window.extensions.embedding.SplitAttributes.SplitType;
import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType;
import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType;
-import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams;
import androidx.window.extensions.embedding.TaskContainer.TaskProperties;
import androidx.window.extensions.layout.DisplayFeature;
import androidx.window.extensions.layout.FoldingFeature;
@@ -268,10 +268,11 @@
container = mController.newContainer(activity, taskId);
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForSplitTaskFragment(bounds);
- createTaskFragment(wct, container.getTaskFragmentToken(), activity.getActivityToken(),
- bounds, windowingMode);
+ final IBinder reparentActivityToken = activity.getActivityToken();
+ createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken,
+ bounds, windowingMode, reparentActivityToken);
wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
- activity.getActivityToken());
+ reparentActivityToken);
} else {
resizeTaskFragmentIfRegistered(wct, container, bounds);
final int windowingMode = mController.getTaskContainer(taskId)
@@ -551,11 +552,12 @@
@NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
final Configuration taskConfiguration = taskProperties.getConfiguration();
final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
- final SplitAttributesCalculator calculator = mController.getSplitAttributesCalculator();
+ final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
+ mController.getSplitAttributesCalculator();
final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
- final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics);
+ final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
if (calculator == null) {
- if (!isDefaultMinSizeSatisfied) {
+ if (!areDefaultConstraintsSatisfied) {
return EXPAND_CONTAINERS_ATTRIBUTES;
}
return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes,
@@ -565,9 +567,9 @@
.getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
taskConfiguration.windowConfiguration);
final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams(
- taskWindowMetrics, taskConfiguration, defaultSplitAttributes,
- isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag());
- final SplitAttributes splitAttributes = calculator.computeSplitAttributesForParams(params);
+ taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes,
+ areDefaultConstraintsSatisfied, rule.getTag());
+ final SplitAttributes splitAttributes = calculator.apply(params);
return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 076856c..17814c6 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -141,12 +141,26 @@
mToken = new Binder("TaskFragmentContainer");
mTaskContainer = taskContainer;
if (pairedPrimaryContainer != null) {
+ // The TaskFragment will be positioned right above the paired container.
if (pairedPrimaryContainer.getTaskContainer() != taskContainer) {
throw new IllegalArgumentException(
"pairedPrimaryContainer must be in the same Task");
}
final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer);
taskContainer.mContainers.add(primaryIndex + 1, this);
+ } else if (pendingAppearedActivity != null) {
+ // The TaskFragment will be positioned right above the pending appeared Activity. If any
+ // existing TaskFragment is empty with pending Intent, it is likely that the Activity of
+ // the pending Intent hasn't been created yet, so the new Activity should be below the
+ // empty TaskFragment.
+ int i = taskContainer.mContainers.size() - 1;
+ for (; i >= 0; i--) {
+ final TaskFragmentContainer container = taskContainer.mContainers.get(i);
+ if (!container.isEmpty() || container.getPendingAppearedIntent() == null) {
+ break;
+ }
+ }
+ taskContainer.mContainers.add(i + 1, this);
} else {
taskContainer.mContainers.add(this);
}
@@ -500,6 +514,8 @@
}
if (!shouldFinishDependent) {
+ // Always finish the placeholder when the primary is finished.
+ finishPlaceholderIfAny(wct, presenter);
return;
}
@@ -526,6 +542,28 @@
mActivitiesToFinishOnExit.clear();
}
+ @GuardedBy("mController.mLock")
+ private void finishPlaceholderIfAny(@NonNull WindowContainerTransaction wct,
+ @NonNull SplitPresenter presenter) {
+ final List<TaskFragmentContainer> containersToRemove = new ArrayList<>();
+ for (TaskFragmentContainer container : mContainersToFinishOnExit) {
+ if (container.mIsFinished) {
+ continue;
+ }
+ final SplitContainer splitContainer = mController.getActiveSplitForContainers(
+ this, container);
+ if (splitContainer != null && splitContainer.isPlaceholderContainer()
+ && splitContainer.getSecondaryContainer() == container) {
+ // Remove the placeholder secondary TaskFragment.
+ containersToRemove.add(container);
+ }
+ }
+ mContainersToFinishOnExit.removeAll(containersToRemove);
+ for (TaskFragmentContainer container : containersToRemove) {
+ container.finish(false /* shouldFinishDependent */, presenter, wct, mController);
+ }
+ }
+
boolean isFinished() {
return mIsFinished;
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index c9f8700..8386131 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -45,6 +45,7 @@
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
+import androidx.window.extensions.core.util.function.Consumer;
import androidx.window.util.DataProducer;
import java.util.ArrayList;
@@ -53,7 +54,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Consumer;
/**
* Reference implementation of androidx.window.extensions.layout OEM interface for use with
@@ -82,6 +82,10 @@
private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners =
new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final Map<java.util.function.Consumer<WindowLayoutInfo>, Consumer<WindowLayoutInfo>>
+ mJavaToExtConsumers = new ArrayMap<>();
+
private final TaskFragmentOrganizer mTaskFragmentOrganizer;
public WindowLayoutComponentImpl(@NonNull Context context,
@@ -95,7 +99,8 @@
}
/** Registers to listen to {@link CommonFoldingFeature} changes */
- public void addFoldingStateChangedCallback(Consumer<List<CommonFoldingFeature>> consumer) {
+ public void addFoldingStateChangedCallback(
+ java.util.function.Consumer<List<CommonFoldingFeature>> consumer) {
synchronized (mLock) {
mFoldingFeatureProducer.addDataChangedCallback(consumer);
}
@@ -109,13 +114,27 @@
*/
@Override
public void addWindowLayoutInfoListener(@NonNull Activity activity,
- @NonNull Consumer<WindowLayoutInfo> consumer) {
- addWindowLayoutInfoListener((Context) activity, consumer);
+ @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
+ final Consumer<WindowLayoutInfo> extConsumer = consumer::accept;
+ synchronized (mLock) {
+ mJavaToExtConsumers.put(consumer, extConsumer);
+ }
+ addWindowLayoutInfoListener(activity, extConsumer);
+ }
+
+ @Override
+ public void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
+ @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
+ final Consumer<WindowLayoutInfo> extConsumer = consumer::accept;
+ synchronized (mLock) {
+ mJavaToExtConsumers.put(consumer, extConsumer);
+ }
+ addWindowLayoutInfoListener(context, extConsumer);
}
/**
- * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context
- * as a parameter.
+ * Similar to {@link #addWindowLayoutInfoListener(Activity, java.util.function.Consumer)}, but
+ * takes a UI Context as a parameter.
*
* Jetpack {@link androidx.window.layout.ExtensionWindowLayoutInfoBackend} makes sure all
* consumers related to the same {@link Context} gets updated {@link WindowLayoutInfo}
@@ -156,6 +175,18 @@
}
}
+ @Override
+ public void removeWindowLayoutInfoListener(
+ @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
+ final Consumer<WindowLayoutInfo> extConsumer;
+ synchronized (mLock) {
+ extConsumer = mJavaToExtConsumers.remove(consumer);
+ }
+ if (extConsumer != null) {
+ removeWindowLayoutInfoListener(extConsumer);
+ }
+ }
+
/**
* Removes a listener no longer interested in receiving updates.
*
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index 2f92a57..459ec9f 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -34,9 +34,11 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Pair;
+import android.view.WindowMetrics;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerToken;
+import androidx.window.extensions.core.util.function.Predicate;
import androidx.window.extensions.embedding.SplitAttributes.SplitType;
import androidx.window.extensions.layout.DisplayFeature;
import androidx.window.extensions.layout.FoldingFeature;
@@ -107,7 +109,7 @@
static SplitRule createSplitRule(@NonNull Activity primaryActivity,
@NonNull Intent secondaryIntent, boolean clearTop) {
final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
- return new SplitPairRule.Builder(
+ return createSplitPairRuleBuilder(
activityPair -> false,
targetPair::equals,
w -> true)
@@ -144,7 +146,7 @@
@NonNull Activity secondaryActivity, int finishPrimaryWithSecondary,
int finishSecondaryWithPrimary, boolean clearTop) {
final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity);
- return new SplitPairRule.Builder(
+ return createSplitPairRuleBuilder(
targetPair::equals,
activityIntentPair -> false,
w -> true)
@@ -223,4 +225,26 @@
displayFeatures.add(foldingFeature);
return new WindowLayoutInfo(displayFeatures);
}
+
+ static ActivityRule.Builder createActivityBuilder(
+ @NonNull Predicate<Activity> activityPredicate,
+ @NonNull Predicate<Intent> intentPredicate) {
+ return new ActivityRule.Builder(activityPredicate, intentPredicate);
+ }
+
+ static SplitPairRule.Builder createSplitPairRuleBuilder(
+ @NonNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate,
+ @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate,
+ @NonNull Predicate<WindowMetrics> windowMetricsPredicate) {
+ return new SplitPairRule.Builder(activitiesPairPredicate, activityIntentPairPredicate,
+ windowMetricsPredicate);
+ }
+
+ static SplitPlaceholderRule.Builder createSplitPlaceholderRuleBuilder(
+ @NonNull Intent placeholderIntent, @NonNull Predicate<Activity> activityPredicate,
+ @NonNull Predicate<Intent> intentPredicate,
+ @NonNull Predicate<WindowMetrics> windowMetricsPredicate) {
+ return new SplitPlaceholderRule.Builder(placeholderIntent, activityPredicate,
+ intentPredicate, windowMetricsPredicate);
+ }
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 81c3957..0bf0bc8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -34,8 +34,11 @@
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityBuilder;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
@@ -432,7 +435,7 @@
@Test
public void testResolveStartActivityIntent_withoutLaunchingActivity() {
final Intent intent = new Intent();
- final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent)
+ final ActivityRule expandRule = createActivityBuilder(r -> false, i -> i == intent)
.setShouldAlwaysExpand(true)
.build();
mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
@@ -1170,7 +1173,7 @@
@Test
public void testHasSamePresentation() {
- SplitPairRule splitRule1 = new SplitPairRule.Builder(
+ SplitPairRule splitRule1 = createSplitPairRuleBuilder(
activityPair -> true,
activityIntentPair -> true,
windowMetrics -> true)
@@ -1178,7 +1181,7 @@
.setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
.setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
.build();
- SplitPairRule splitRule2 = new SplitPairRule.Builder(
+ SplitPairRule splitRule2 = createSplitPairRuleBuilder(
activityPair -> true,
activityIntentPair -> true,
windowMetrics -> true)
@@ -1191,7 +1194,7 @@
SplitController.haveSamePresentation(splitRule1, splitRule2,
new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
- splitRule2 = new SplitPairRule.Builder(
+ splitRule2 = createSplitPairRuleBuilder(
activityPair -> true,
activityIntentPair -> true,
windowMetrics -> true)
@@ -1355,7 +1358,7 @@
/** Setups a rule to always expand the given intent. */
private void setupExpandRule(@NonNull Intent expandIntent) {
- final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals)
+ final ActivityRule expandRule = createActivityBuilder(r -> false, expandIntent::equals)
.setShouldAlwaysExpand(true)
.build();
mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
@@ -1363,7 +1366,7 @@
/** Setups a rule to always expand the given activity. */
private void setupExpandRule(@NonNull Activity expandActivity) {
- final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false)
+ final ActivityRule expandRule = createActivityBuilder(expandActivity::equals, i -> false)
.setShouldAlwaysExpand(true)
.build();
mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
@@ -1371,7 +1374,7 @@
/** Setups a rule to launch placeholder for the given activity. */
private void setupPlaceholderRule(@NonNull Activity primaryActivity) {
- final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT,
+ final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT,
primaryActivity::equals, i -> false, w -> true)
.setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
.build();
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 121e813..a288fd6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -28,6 +28,7 @@
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
@@ -76,6 +77,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
+import androidx.window.extensions.core.util.function.Function;
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import androidx.window.extensions.layout.WindowLayoutInfo;
@@ -511,7 +513,7 @@
final Activity secondaryActivity = createMockActivity();
final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID);
final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
- final SplitPairRule rule = new SplitPairRule.Builder(pair ->
+ final SplitPairRule rule = createSplitPairRuleBuilder(pair ->
pair.first == mActivity && pair.second == secondaryActivity, pair -> false,
metrics -> true)
.setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
@@ -529,7 +531,7 @@
@Test
public void testComputeSplitAttributes() {
- final SplitPairRule splitPairRule = new SplitPairRule.Builder(
+ final SplitPairRule splitPairRule = createSplitPairRuleBuilder(
activityPair -> true,
activityIntentPair -> true,
windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS))
@@ -561,10 +563,10 @@
SplitAttributes.SplitType.RatioSplitType.splitEqually()
)
).build();
+ final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
+ params -> splitAttributes;
- mController.setSplitAttributesCalculator(params -> {
- return splitAttributes;
- });
+ mController.setSplitAttributesCalculator(calculator);
assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties,
splitPairRule, null /* minDimensionsPair */));
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 7d9d8b0..78b85e6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -154,17 +154,52 @@
null /* pendingAppearedIntent */, taskContainer, mController,
null /* pairedPrimaryContainer */);
doReturn(container1).when(mController).getContainerWithActivity(mActivity);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
// The activity is requested to be reparented, so don't finish it.
- container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+ container0.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
verify(mTransaction, never()).finishActivity(any());
- verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken());
+ verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken());
verify(mController).removeContainer(container0);
}
@Test
+ public void testFinish_alwaysFinishPlaceholder() {
+ // Register container1 as a placeholder
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
+ null /* pendingAppearedIntent */, taskContainer, mController,
+ null /* pairedPrimaryContainer */);
+ final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity);
+ container0.setInfo(mTransaction, info0);
+ final Activity placeholderActivity = createMockActivity();
+ final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity,
+ null /* pendingAppearedIntent */, taskContainer, mController,
+ null /* pairedPrimaryContainer */);
+ final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity);
+ container1.setInfo(mTransaction, info1);
+ final SplitAttributes splitAttributes = new SplitAttributes.Builder().build();
+ final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(new Intent(),
+ mActivity::equals, (java.util.function.Predicate) i -> false,
+ (java.util.function.Predicate) w -> true)
+ .setDefaultSplitAttributes(splitAttributes)
+ .build();
+ mController.registerSplit(mTransaction, container0, mActivity, container1, rule,
+ splitAttributes);
+
+ // The placeholder TaskFragment should be finished even if the primary is finished with
+ // shouldFinishDependent = false.
+ container0.finish(false /* shouldFinishDependent */, mPresenter, mTransaction, mController);
+
+ assertTrue(container0.isFinished());
+ assertTrue(container1.isFinished());
+ verify(mPresenter).deleteTaskFragment(mTransaction, container0.getTaskFragmentToken());
+ verify(mPresenter).deleteTaskFragment(mTransaction, container1.getTaskFragmentToken());
+ verify(mController).removeContainer(container0);
+ verify(mController).removeContainer(container1);
+ }
+
+ @Test
public void testSetInfo() {
final TaskContainer taskContainer = createTestTaskContainer();
// Pending activity should be cleared when it has appeared on server side.
@@ -493,8 +528,6 @@
final TaskFragmentContainer tf1 = new TaskFragmentContainer(
null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
null /* pairedPrimaryTaskFragment */);
- taskContainer.mContainers.add(tf0);
- taskContainer.mContainers.add(tf1);
// When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted
// right above tf0.
@@ -506,6 +539,26 @@
}
@Test
+ public void testNewContainerWithPairedPendingAppearedActivity() {
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer tf0 = new TaskFragmentContainer(
+ createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
+ null /* pairedPrimaryTaskFragment */);
+ final TaskFragmentContainer tf1 = new TaskFragmentContainer(
+ null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
+ null /* pairedPrimaryTaskFragment */);
+
+ // When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any
+ // TaskFragment without any Activity.
+ final TaskFragmentContainer tf2 = new TaskFragmentContainer(
+ createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
+ null /* pairedPrimaryTaskFragment */);
+ assertEquals(0, taskContainer.indexOf(tf0));
+ assertEquals(1, taskContainer.indexOf(tf2));
+ assertEquals(2, taskContainer.indexOf(tf1));
+ }
+
+ @Test
public void testIsVisible() {
final TaskContainer taskContainer = createTestTaskContainer();
final TaskFragmentContainer container = new TaskFragmentContainer(
diff --git a/libs/WindowManager/Jetpack/window-extensions-core-release.aar b/libs/WindowManager/Jetpack/window-extensions-core-release.aar
new file mode 100644
index 0000000..96ff840
--- /dev/null
+++ b/libs/WindowManager/Jetpack/window-extensions-core-release.aar
Binary files differ
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 84ab448..cddbf469 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index f615ad6..0f45219 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -47,6 +47,7 @@
"src/com/android/wm/shell/sysui/ShellSharedConstants.java",
"src/com/android/wm/shell/common/TransactionPool.java",
"src/com/android/wm/shell/animation/Interpolators.java",
+ "src/com/android/wm/shell/pip/PipContentOverlay.java",
"src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java",
],
path: "src",
diff --git a/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml b/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml
new file mode 100644
index 0000000..a3ca74f
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/letterbox_restart_button_background_ripple.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_900" android:alpha="0.6" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml
new file mode 100644
index 0000000..a3ca74f
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/letterbox_restart_dismiss_button_background_ripple.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/system_neutral1_900" android:alpha="0.6" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml
new file mode 100644
index 0000000..6114ad6
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_decor_title.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<shape android:shape="rectangle"
+ android:tintMode="multiply"
+ android:tint="@color/decor_title_color"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/white" />
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
index 0bcaa53..91edbf1 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_minimize_button_dark.xml
@@ -14,11 +14,11 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24"
- android:tint="?attr/colorControlNormal">
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+ android:tint="@color/decor_button_dark_color">
<path
android:fillColor="@android:color/white" android:pathData="M6,21V19H18V21Z"/>
</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
index 416287d..53a8bb1 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml
@@ -17,5 +17,4 @@
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
- <corners android:radius="20dp" />
</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background.xml
new file mode 100644
index 0000000..60f3cfe
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorAccentPrimaryVariant"/>
+ <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/>
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml
new file mode 100644
index 0000000..ef97ea1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/letterbox_restart_button_background_ripple">
+ <item android:drawable="@drawable/letterbox_restart_button_background"/>
+</ripple>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml
new file mode 100644
index 0000000..c247c6e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_button.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:state_checked="true"
+ android:drawable="@drawable/letterbox_restart_checkbox_checked" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/letterbox_restart_checkbox_checked" />
+ <item android:state_pressed="false"
+ android:drawable="@drawable/letterbox_restart_checkbox_unchecked" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml
new file mode 100644
index 0000000..4f97e2c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_checked.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20"
+ android:tint="?android:attr/textColorSecondary">
+ <group
+ android:scaleX="0.83333333333"
+ android:scaleY="0.83333333333"
+ android:translateX="0"
+ android:translateY="0">
+ <path
+ android:fillColor="?android:attr/textColorSecondary"
+ android:pathData="M10.6,16.2 L17.65,9.15 16.25,7.75 10.6,13.4 7.75,10.55 6.35,11.95ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml
new file mode 100644
index 0000000..bb14d19
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_checkbox_unchecked.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20"
+ android:tint="?android:attr/textColorSecondary">
+ <group
+ android:scaleX="0.83333333333"
+ android:scaleY="0.83333333333"
+ android:translateX="0"
+ android:translateY="0">
+ <path
+ android:fillColor="?android:attr/textColorSecondary"
+ android:pathData="M5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml
new file mode 100644
index 0000000..e3c18a2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorSurface"/>
+ <corners android:radius="@dimen/letterbox_restart_dialog_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background.xml
new file mode 100644
index 0000000..af89d41
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant" android:width="1dp"/>
+ <solid android:color="?androidprv:attr/colorSurface" />
+ <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/>
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml
new file mode 100644
index 0000000..e32aefc
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/letterbox_restart_dismiss_button_background_ripple">
+ <item android:drawable="@drawable/letterbox_restart_dismiss_button_background"/>
+</ripple>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml
new file mode 100644
index 0000000..5053971
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="@dimen/letterbox_restart_dialog_title_icon_width"
+ android:height="@dimen/letterbox_restart_dialog_title_icon_height"
+ android:viewportWidth="45"
+ android:viewportHeight="44">
+ <group
+ android:scaleX="0.8"
+ android:scaleY="0.8"
+ android:translateX="8"
+ android:translateY="8">
+ <path
+ android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
+ android:fillColor="?androidprv:attr/colorAccentPrimaryVariant"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml
new file mode 100644
index 0000000..b6e0172
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_ic_arrows.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/letterbox_restart_dialog_title_icon_width"
+ android:height="@dimen/letterbox_restart_dialog_title_icon_height"
+ android:viewportWidth="45"
+ android:viewportHeight="44">
+ <group
+ android:scaleX="0.8"
+ android:scaleY="0.8"
+ android:translateX="8"
+ android:translateY="8">
+ <path
+ android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z"
+ android:fillColor="@color/compat_controls_text"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decor.xml b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml
new file mode 100644
index 0000000..f3d2198
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/caption_window_decor.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.wm.shell.windowdecor.WindowDecorLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/caption"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="end"
+ android:background="@drawable/caption_decor_title">
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/back_button"
+ android:layout_gravity="center_vertical|end"
+ android:contentDescription="@string/back_button_text"
+ android:background="@drawable/decor_back_button_dark"
+ android:duplicateParentState="true"/>
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:elevation="2dp"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/minimize_window"
+ android:layout_gravity="center_vertical|end"
+ android:contentDescription="@string/minimize_button_text"
+ android:background="@drawable/decor_minimize_button_dark"
+ android:duplicateParentState="true"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/maximize_window"
+ android:layout_gravity="center_vertical|end"
+ android:contentDescription="@string/maximize_button_text"
+ android:background="@drawable/decor_maximize_button_dark"
+ android:duplicateParentState="true"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/close_window"
+ android:contentDescription="@string/close_button_text"
+ android:background="@drawable/decor_close_button_dark"
+ android:duplicateParentState="true"/>
+</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
index 2a4cc02..da31a46 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml
@@ -17,26 +17,14 @@
<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/desktop_mode_caption"
- android:layout_width="wrap_content"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
android:background="@drawable/desktop_mode_decor_title">
<Button
- style="@style/CaptionButtonStyle"
- android:id="@+id/back_button"
- android:contentDescription="@string/back_button_text"
- android:background="@drawable/decor_back_button_dark"
- />
- <Button
android:id="@+id/caption_handle"
android:layout_width="128dp"
android:layout_height="32dp"
- android:layout_margin="5dp"
- android:padding="4dp"
android:contentDescription="@string/handle_text"
android:background="@drawable/decor_handle_dark"/>
- <Button
- style="@style/CaptionButtonStyle"
- android:id="@+id/close_window"
- android:contentDescription="@string/close_button_text"
- android:background="@drawable/decor_close_button_dark"/>
</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
new file mode 100644
index 0000000..ba9852c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml
@@ -0,0 +1,142 @@
+<!--
+ ~ 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.wm.shell.compatui.RestartDialogLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/system_neutral1_900">
+
+ <!-- The background of the top-level layout acts as the background dim. -->
+
+ <!--TODO (b/266288912): Resolve overdraw warning -->
+
+ <!-- Vertical margin will be set dynamically since it depends on task bounds.
+ Setting the alpha of the dialog container to 0, since it shouldn't be visible until the
+ enter animation starts. -->
+ <FrameLayout
+ android:id="@+id/letterbox_restart_dialog_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/letterbox_restart_dialog_margin"
+ android:background="@drawable/letterbox_restart_dialog_background"
+ android:alpha="0"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintWidth_max="@dimen/letterbox_restart_dialog_width">
+
+ <!-- The ScrollView should only wrap the content of the dialog, otherwise the background
+ corner radius will be cut off when scrolling to the top/bottom. -->
+
+ <ScrollView android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:padding="24dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageView
+ android:importantForAccessibility="no"
+ android:layout_width="@dimen/letterbox_restart_dialog_title_icon_width"
+ android:layout_height="@dimen/letterbox_restart_dialog_title_icon_height"
+ android:src="@drawable/letterbox_restart_header_ic_arrows"/>
+
+ <TextView
+ android:layout_marginVertical="16dp"
+ android:id="@+id/letterbox_restart_dialog_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/letterbox_restart_dialog_title"
+ android:textAlignment="center"
+ android:textAppearance="@style/RestartDialogTitleText"/>
+
+ <TextView
+ android:textAppearance="@style/RestartDialogBodyText"
+ android:id="@+id/letterbox_restart_dialog_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/letterbox_restart_dialog_description"
+ android:textAlignment="center"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="center_vertical"
+ android:layout_marginVertical="32dp">
+
+ <CheckBox
+ android:id="@+id/letterbox_restart_dialog_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:button="@drawable/letterbox_restart_checkbox_button"/>
+
+ <TextView
+ android:textAppearance="@style/RestartDialogCheckboxText"
+ android:layout_marginStart="12dp"
+ android:id="@+id/letterbox_restart_dialog_checkbox_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/letterbox_restart_dialog_checkbox_title"
+ android:textAlignment="textStart"/>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:minHeight="@dimen/letterbox_restart_dialog_button_height"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:textAppearance="@style/RestartDialogDismissButton"
+ android:id="@+id/letterbox_restart_dialog_dismiss_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/letterbox_restart_dialog_button_width"
+ android:minHeight="@dimen/letterbox_restart_dialog_button_height"
+ android:layout_gravity="start"
+ android:background=
+ "@drawable/letterbox_restart_dismiss_button_background_ripple"
+ android:text="@string/letterbox_restart_cancel"
+ android:contentDescription="@string/letterbox_restart_cancel"/>
+
+ <Button
+ android:textAppearance="@style/RestartDialogConfirmButton"
+ android:id="@+id/letterbox_restart_dialog_restart_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/letterbox_restart_dialog_button_width"
+ android:minHeight="@dimen/letterbox_restart_dialog_button_height"
+ android:layout_gravity="end"
+ android:background=
+ "@drawable/letterbox_restart_button_background_ripple"
+ android:text="@string/letterbox_restart_restart"
+ android:contentDescription="@string/letterbox_restart_restart"/>
+
+ </FrameLayout>
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ </FrameLayout>
+
+</com.android.wm.shell.compatui.RestartDialogLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 3ee20ea..8908959 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -273,6 +273,39 @@
<!-- The space between two actions in the letterbox education dialog -->
<dimen name="letterbox_education_dialog_space_between_actions">24dp</dimen>
+ <!-- The margin between the dialog container and its parent. -->
+ <dimen name="letterbox_restart_dialog_margin">24dp</dimen>
+
+ <!-- The corner radius of the restart confirmation dialog. -->
+ <dimen name="letterbox_restart_dialog_corner_radius">28dp</dimen>
+
+ <!-- The fixed width of the dialog if there is enough space in the parent. -->
+ <dimen name="letterbox_restart_dialog_width">348dp</dimen>
+
+ <!-- The width of the top icon in the restart confirmation dialog. -->
+ <dimen name="letterbox_restart_dialog_title_icon_width">32dp</dimen>
+
+ <!-- The height of the top icon in the restart confirmation dialog. -->
+ <dimen name="letterbox_restart_dialog_title_icon_height">32dp</dimen>
+
+ <!-- The width of an icon in the restart confirmation dialog. -->
+ <dimen name="letterbox_restart_dialog_icon_width">40dp</dimen>
+
+ <!-- The height of an icon in the restart confirmation dialog. -->
+ <dimen name="letterbox_restart_dialog_icon_height">32dp</dimen>
+
+ <!-- The space between two actions in the restart confirmation dialog -->
+ <dimen name="letterbox_restart_dialog_space_between_actions">24dp</dimen>
+
+ <!-- The width of the buttons in the restart dialog -->
+ <dimen name="letterbox_restart_dialog_button_width">82dp</dimen>
+
+ <!-- The width of the buttons in the restart dialog -->
+ <dimen name="letterbox_restart_dialog_button_height">36dp</dimen>
+
+ <!-- The corner radius of the buttons in the restart dialog -->
+ <dimen name="letterbox_restart_dialog_button_radius">18dp</dimen>
+
<!-- The width of the brand image on staring surface. -->
<dimen name="starting_surface_brand_image_width">200dp</dimen>
@@ -331,8 +364,8 @@
<!-- Height of button (32dp) + 2 * margin (5dp each). -->
<dimen name="freeform_decor_caption_height">42dp</dimen>
- <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). -->
- <dimen name="freeform_decor_caption_width">216dp</dimen>
+ <!-- Width of buttons (32dp each) + padding (128dp total). -->
+ <dimen name="freeform_decor_caption_menu_width">256dp</dimen>
<dimen name="freeform_resize_handle">30dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 25eddf8..250dac6 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -188,6 +188,23 @@
<!-- Accessibility description of the letterbox education toast expand to dialog button. [CHAR LIMIT=NONE] -->
<string name="letterbox_education_expand_button_description">Expand for more information.</string>
+ <!-- The title of the restart confirmation dialog. [CHAR LIMIT=NONE] -->
+ <string name="letterbox_restart_dialog_title">Restart for a better view?</string>
+
+ <!-- The description of the restart confirmation dialog. [CHAR LIMIT=NONE] -->
+ <string name="letterbox_restart_dialog_description">You can restart the app so it looks better on
+ your screen, but you may lose your progress or any unsaved changes
+ </string>
+
+ <!-- Button text for dismissing the restart confirmation dialog. [CHAR LIMIT=20] -->
+ <string name="letterbox_restart_cancel">Cancel</string>
+
+ <!-- Button text for dismissing the restart confirmation dialog. [CHAR LIMIT=20] -->
+ <string name="letterbox_restart_restart">Restart</string>
+
+ <!-- Checkbox text for asking to not show the restart confirmation dialog again. [CHAR LIMIT=NONE] -->
+ <string name="letterbox_restart_dialog_checkbox_title">Don\u2019t show again</string>
+
<!-- Freeform window caption strings -->
<!-- Accessibility text for the maximize window button [CHAR LIMIT=NONE] -->
<string name="maximize_button_text">Maximize</string>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index a859721..e8f340c 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -63,4 +63,42 @@
<item name="android:lineHeight">16sp</item>
<item name="android:textColor">@color/tv_pip_edu_text</item>
</style>
+
+ <style name="RestartDialogTitleText">
+ <item name="android:textSize">24sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:lineSpacingExtra">2sp</item>
+ <item name="android:textAppearance">
+ @*android:style/TextAppearance.DeviceDefault.Headline
+ </item>
+ </style>
+
+ <style name="RestartDialogBodyText">
+ <item name="android:textSize">14sp</item>
+ <item name="android:letterSpacing">0.02</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:lineSpacingExtra">2sp</item>
+ <item name="android:textAppearance">
+ @*android:style/TextAppearance.DeviceDefault.Body2
+ </item>
+ </style>
+
+ <style name="RestartDialogCheckboxText">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:lineSpacingExtra">4sp</item>
+ <item name="android:textAppearance">@*android:style/TextAppearance.DeviceDefault</item>
+ </style>
+
+ <style name="RestartDialogDismissButton">
+ <item name="android:lineSpacingExtra">2sp</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="RestartDialogConfirmButton">
+ <item name="android:lineSpacingExtra">2sp</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ </style>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index e58e785..97a9fed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -256,12 +256,30 @@
}
}
+ /**
+ * Creates a persistent root task in WM for a particular windowing-mode.
+ * @param displayId The display to create the root task on.
+ * @param windowingMode Windowing mode to put the root task in.
+ * @param listener The listener to get the created task callback.
+ */
public void createRootTask(int displayId, int windowingMode, TaskListener listener) {
- ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s",
+ createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */);
+ }
+
+ /**
+ * Creates a persistent root task in WM for a particular windowing-mode.
+ * @param displayId The display to create the root task on.
+ * @param windowingMode Windowing mode to put the root task in.
+ * @param listener The listener to get the created task callback.
+ * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed.
+ */
+ public void createRootTask(int displayId, int windowingMode, TaskListener listener,
+ boolean removeWithTaskOrganizer) {
+ ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" ,
displayId, windowingMode, listener.toString());
final IBinder cookie = new Binder();
setPendingLaunchCookieListener(cookie, listener);
- super.createRootTask(displayId, windowingMode, cookie);
+ super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index cbcd949..aaeef19 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -51,6 +51,7 @@
import android.view.SurfaceControl;
import android.window.BackAnimationAdaptor;
import android.window.BackEvent;
+import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
import android.window.IBackAnimationRunner;
import android.window.IBackNaviAnimationController;
@@ -81,7 +82,7 @@
/** Flag for U animation features */
public static boolean IS_U_ANIMATION_ENABLED =
SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
- SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+ SETTING_VALUE_ON) == SETTING_VALUE_ON;
/** Predictive back animation developer option */
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
// TODO (b/241808055) Find a appropriate time to remove during refactor
@@ -173,11 +174,11 @@
boolean consumed = false;
if (mWaitingAnimation && mOnBackCallback != null) {
if (mTriggerBack) {
- final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
+ final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(1);
dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
dispatchOnBackInvoked(mOnBackCallback);
} else {
- final BackEvent backFinish = mTouchTracker.createProgressEvent(0);
+ final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(0);
dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
dispatchOnBackCancelled(mOnBackCallback);
}
@@ -480,7 +481,7 @@
if (!mBackGestureStarted || mBackNavigationInfo == null) {
return;
}
- final BackEvent backEvent = mTouchTracker.createProgressEvent();
+ final BackMotionEvent backEvent = mTouchTracker.createProgressEvent();
if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) {
dispatchOnBackProgressed(mBackToLauncherCallback, backEvent);
} else if (mEnableAnimations.get()) {
@@ -573,7 +574,7 @@
}
private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
- BackEvent backEvent) {
+ BackMotionEvent backEvent) {
if (callback == null) {
return;
}
@@ -611,7 +612,7 @@
}
private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
- BackEvent backEvent) {
+ BackMotionEvent backEvent) {
if (callback == null) {
return;
}
@@ -730,7 +731,7 @@
}
dispatchOnBackStarted(mBackToLauncherCallback,
mTouchTracker.createStartEvent(mAnimationTarget));
- final BackEvent backInit = mTouchTracker.createProgressEvent();
+ final BackMotionEvent backInit = mTouchTracker.createProgressEvent();
if (!mCachingBackDispatcher.consume()) {
dispatchOnBackProgressed(mBackToLauncherCallback, backInit);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
index ccfac65..695ef4e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -19,6 +19,7 @@
import android.os.SystemProperties;
import android.view.RemoteAnimationTarget;
import android.window.BackEvent;
+import android.window.BackMotionEvent;
/**
* Helper class to record the touch location for gesture and generate back events.
@@ -82,11 +83,11 @@
mSwipeEdge = BackEvent.EDGE_LEFT;
}
- BackEvent createStartEvent(RemoteAnimationTarget target) {
- return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+ BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
+ return new BackMotionEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
}
- BackEvent createProgressEvent() {
+ BackMotionEvent createProgressEvent() {
float progressThreshold = PROGRESS_THRESHOLD >= 0
? PROGRESS_THRESHOLD : mProgressThreshold;
progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
@@ -109,8 +110,8 @@
return createProgressEvent(progress);
}
- BackEvent createProgressEvent(float progress) {
- return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+ BackMotionEvent createProgressEvent(float progress) {
+ return new BackMotionEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
}
public void setProgressThreshold(float progressThreshold) {
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 09dc68a..e24c228 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
@@ -21,6 +21,7 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import android.annotation.DimenRes;
+import android.annotation.Hide;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
@@ -125,7 +126,7 @@
private Icon mIcon;
private boolean mIsBubble;
private boolean mIsTextChanged;
- private boolean mIsClearable;
+ private boolean mIsDismissable;
private boolean mShouldSuppressNotificationDot;
private boolean mShouldSuppressNotificationList;
private boolean mShouldSuppressPeek;
@@ -180,7 +181,7 @@
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
final int desiredHeight, final int desiredHeightResId, @Nullable final String title,
- int taskId, @Nullable final String locus, Executor mainExecutor,
+ int taskId, @Nullable final String locus, boolean isDismissable, Executor mainExecutor,
final Bubbles.BubbleMetadataFlagListener listener) {
Objects.requireNonNull(key);
Objects.requireNonNull(shortcutInfo);
@@ -189,6 +190,7 @@
mKey = key;
mGroupKey = null;
mLocusId = locus != null ? new LocusId(locus) : null;
+ mIsDismissable = isDismissable;
mFlags = 0;
mUser = shortcutInfo.getUserHandle();
mPackageName = shortcutInfo.getPackage();
@@ -245,6 +247,11 @@
return mKey;
}
+ @Hide
+ public boolean isDismissable() {
+ return mIsDismissable;
+ }
+
/**
* @see StatusBarNotification#getGroupKey()
* @return the group key for this bubble, if one exists.
@@ -526,7 +533,7 @@
mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
}
- mIsClearable = entry.isClearable();
+ mIsDismissable = entry.isDismissable();
mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot();
mShouldSuppressNotificationList = entry.shouldSuppressNotificationList();
mShouldSuppressPeek = entry.shouldSuppressPeek();
@@ -605,7 +612,7 @@
* Whether this notification should be shown in the shade.
*/
boolean showInShade() {
- return !shouldSuppressNotification() || !mIsClearable;
+ return !shouldSuppressNotification() || !mIsDismissable;
}
/**
@@ -870,7 +877,7 @@
pw.print(" desiredHeight: "); pw.println(getDesiredHeightString());
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
- pw.print(" isClearable: "); pw.println(mIsClearable);
+ pw.print(" isDismissable: "); pw.println(mIsDismissable);
pw.println(" bubbleMetadataFlagListener null: " + (mBubbleMetadataFlagListener == null));
if (mExpandedView != null) {
mExpandedView.dump(pw);
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 dd8afff..71e15c1 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
@@ -973,21 +973,59 @@
}
/**
- * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n
- * otification and remain until the user dismisses the bubble or bubble stack. Only one intent
- * bubble is supported at a time.
+ * This method has different behavior depending on:
+ * - if an app bubble exists
+ * - if an app bubble is expanded
+ *
+ * If no app bubble exists, this will add and expand a bubble with the provided intent. The
+ * intent must be explicit (i.e. include a package name or fully qualified component class name)
+ * and the activity for it should be resizable.
+ *
+ * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
+ * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
+ * this method will expand it.
+ *
+ * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
+ * the bubble or bubble stack.
+ *
+ * Some notes:
+ * - Only one app bubble is supported at a time
+ * - Calling this method with a different intent than the existing app bubble will do nothing
*
* @param intent the intent to display in the bubble expanded view.
*/
- public void showAppBubble(Intent intent) {
- if (intent == null || intent.getPackage() == null) return;
+ public void showOrHideAppBubble(Intent intent) {
+ if (intent == null || intent.getPackage() == null) {
+ Log.w(TAG, "App bubble failed to show, invalid intent: " + intent
+ + ((intent != null) ? " with package: " + intent.getPackage() : " "));
+ return;
+ }
PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId);
if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return;
- Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
- b.setShouldAutoExpand(true);
- inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+ Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE);
+ if (existingAppBubble != null) {
+ BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
+ if (isStackExpanded()) {
+ if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) {
+ // App bubble is expanded, lets collapse
+ collapseStack();
+ } else {
+ // App bubble is not selected, select it
+ mBubbleData.setSelectedBubble(existingAppBubble);
+ }
+ } else {
+ // App bubble is not selected, select it & expand
+ mBubbleData.setSelectedBubble(existingAppBubble);
+ mBubbleData.setExpanded(true);
+ }
+ } else {
+ // App bubble does not exist, lets add and expand it
+ Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
+ b.setShouldAutoExpand(true);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+ }
}
/**
@@ -1697,9 +1735,9 @@
}
@Override
- public void showAppBubble(Intent intent) {
+ public void showOrHideAppBubble(Intent intent) {
mMainExecutor.execute(() -> {
- BubbleController.this.showAppBubble(intent);
+ BubbleController.this.showOrHideAppBubble(intent);
});
}
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 af31391..6230d22 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
@@ -17,6 +17,7 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -684,7 +685,8 @@
if (bubble.getPendingIntentCanceled()
|| !(reason == Bubbles.DISMISS_AGED
|| reason == Bubbles.DISMISS_USER_GESTURE
- || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
+ || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)
+ || KEY_APP_BUBBLE.equals(bubble.getKey())) {
return;
}
if (DEBUG_BUBBLE_DATA) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index 3a59614..e37c785 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -109,7 +109,8 @@
b.rawDesiredHeightResId,
b.title,
b.taskId,
- b.locusId?.id
+ b.locusId?.id,
+ b.isDismissable
)
}
}
@@ -205,6 +206,7 @@
entity.title,
entity.taskId,
entity.locus,
+ entity.isDismissable,
mainExecutor,
bubbleMetadataFlagListener
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
index 5f42826..afe19c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEntry.java
@@ -38,18 +38,18 @@
private StatusBarNotification mSbn;
private Ranking mRanking;
- private boolean mIsClearable;
+ private boolean mIsDismissable;
private boolean mShouldSuppressNotificationDot;
private boolean mShouldSuppressNotificationList;
private boolean mShouldSuppressPeek;
public BubbleEntry(@NonNull StatusBarNotification sbn,
- Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot,
+ Ranking ranking, boolean isDismissable, boolean shouldSuppressNotificationDot,
boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) {
mSbn = sbn;
mRanking = ranking;
- mIsClearable = isClearable;
+ mIsDismissable = isDismissable;
mShouldSuppressNotificationDot = shouldSuppressNotificationDot;
mShouldSuppressNotificationList = shouldSuppressNotificationList;
mShouldSuppressPeek = shouldSuppressPeek;
@@ -115,9 +115,9 @@
return mRanking.canBubble();
}
- /** @return true if this notification is clearable. */
- public boolean isClearable() {
- return mIsClearable;
+ /** @return true if this notification can be dismissed. */
+ public boolean isDismissable() {
+ return mIsDismissable;
}
/** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */
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 465d1ab..df43257 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
@@ -109,13 +109,28 @@
void expandStackAndSelectBubble(Bubble bubble);
/**
- * Adds and expands bubble that is not notification based, but instead based on an intent from
- * the app. The intent must be explicit (i.e. include a package name or fully qualified
- * component class name) and the activity for it should be resizable.
+ * This method has different behavior depending on:
+ * - if an app bubble exists
+ * - if an app bubble is expanded
*
- * @param intent the intent to populate the bubble.
+ * If no app bubble exists, this will add and expand a bubble with the provided intent. The
+ * intent must be explicit (i.e. include a package name or fully qualified component class name)
+ * and the activity for it should be resizable.
+ *
+ * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
+ * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
+ * this method will expand it.
+ *
+ * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
+ * the bubble or bubble stack.
+ *
+ * Some notes:
+ * - Only one app bubble is supported at a time
+ * - Calling this method with a different intent than the existing app bubble will do nothing
+ *
+ * @param intent the intent to display in the bubble expanded view.
*/
- void showAppBubble(Intent intent);
+ void showOrHideAppBubble(Intent intent);
/**
* @return a bubble that matches the provided shortcutId, if one exists.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
index 186b9b1..9b2e263 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleEntity.kt
@@ -27,5 +27,6 @@
@DimenRes val desiredHeightResId: Int,
val title: String? = null,
val taskId: Int,
- val locus: String? = null
+ val locus: String? = null,
+ val isDismissable: Boolean = false
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
index f4fa183..48d8ccf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelper.kt
@@ -43,6 +43,7 @@
private const val ATTR_TITLE = "t"
private const val ATTR_TASK_ID = "tid"
private const val ATTR_LOCUS = "l"
+private const val ATTR_DISMISSABLE = "d"
/**
* Writes the bubbles in xml format into given output stream.
@@ -84,6 +85,7 @@
bubble.title?.let { serializer.attribute(null, ATTR_TITLE, it) }
serializer.attribute(null, ATTR_TASK_ID, bubble.taskId.toString())
bubble.locus?.let { serializer.attribute(null, ATTR_LOCUS, it) }
+ serializer.attribute(null, ATTR_DISMISSABLE, bubble.isDismissable.toString())
serializer.endTag(null, TAG_BUBBLE)
} catch (e: IOException) {
throw RuntimeException(e)
@@ -142,7 +144,8 @@
parser.getAttributeWithName(ATTR_DESIRED_HEIGHT_RES_ID)?.toInt() ?: return null,
parser.getAttributeWithName(ATTR_TITLE),
parser.getAttributeWithName(ATTR_TASK_ID)?.toInt() ?: INVALID_TASK_ID,
- parser.getAttributeWithName(ATTR_LOCUS)
+ parser.getAttributeWithName(ATTR_LOCUS),
+ parser.getAttributeWithName(ATTR_DISMISSABLE)?.toBoolean() ?: false
)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index 96efeeb..8484013 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -96,8 +96,7 @@
/**
* Different from {@link #equals(Object)}, this method compares the basic geometry properties
- * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout and
- * insets.
+ * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout.
* @return {@code true} if the given {@link DisplayLayout} is identical geometry wise.
*/
public boolean isSameGeometry(@NonNull DisplayLayout other) {
@@ -105,8 +104,7 @@
&& mHeight == other.mHeight
&& mRotation == other.mRotation
&& mDensityDpi == other.mDensityDpi
- && Objects.equals(mCutout, other.mCutout)
- && Objects.equals(mStableInsets, other.mStableInsets);
+ && Objects.equals(mCutout, other.mCutout);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index a9d3c9f..abb357c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -78,6 +78,7 @@
private final Rect mResizingBounds = new Rect();
private final Rect mTempRect = new Rect();
private ValueAnimator mFadeAnimator;
+ private ValueAnimator mScreenshotAnimator;
private int mIconSize;
private int mOffsetX;
@@ -135,8 +136,17 @@
/** Releases the surfaces for split decor. */
public void release(SurfaceControl.Transaction t) {
- if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
- mFadeAnimator.cancel();
+ if (mFadeAnimator != null) {
+ if (mFadeAnimator.isRunning()) {
+ mFadeAnimator.cancel();
+ }
+ mFadeAnimator = null;
+ }
+ if (mScreenshotAnimator != null) {
+ if (mScreenshotAnimator.isRunning()) {
+ mScreenshotAnimator.cancel();
+ }
+ mScreenshotAnimator = null;
}
if (mViewHost != null) {
mViewHost.release();
@@ -238,16 +248,20 @@
/** Stops showing resizing hint. */
public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) {
if (mScreenshot != null) {
+ if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
+ mScreenshotAnimator.cancel();
+ }
+
t.setPosition(mScreenshot, mOffsetX, mOffsetY);
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
- final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
- va.addUpdateListener(valueAnimator -> {
+ mScreenshotAnimator = ValueAnimator.ofFloat(1, 0);
+ mScreenshotAnimator.addUpdateListener(valueAnimator -> {
final float progress = (float) valueAnimator.getAnimatedValue();
animT.setAlpha(mScreenshot, progress);
animT.apply();
});
- va.addListener(new AnimatorListenerAdapter() {
+ mScreenshotAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mRunningAnimationCount++;
@@ -266,7 +280,7 @@
}
}
});
- va.start();
+ mScreenshotAnimator.start();
}
if (mResizingIconView == null) {
@@ -292,9 +306,6 @@
});
return;
}
-
- // If fade-in animation is running, cancel it and re-run fade-out one.
- mFadeAnimator.cancel();
}
if (mShown) {
fadeOutDecor(animFinishedCallback);
@@ -332,6 +343,11 @@
* directly. */
public void fadeOutDecor(Runnable finishedCallback) {
if (mShown) {
+ // If previous animation is running, just cancel it.
+ if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
+ mFadeAnimator.cancel();
+ }
+
startFadeAnimation(false /* show */, true, finishedCallback);
mShown = false;
} else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
index 4f33a71..06f0a70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -16,11 +16,12 @@
package com.android.wm.shell.compatui;
+import android.annotation.NonNull;
+import android.app.TaskInfo;
import android.content.Context;
+import android.content.SharedPreferences;
import android.provider.DeviceConfig;
-import androidx.annotation.NonNull;
-
import com.android.wm.shell.R;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ShellMainThread;
@@ -34,11 +35,27 @@
@WMSingleton
public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener {
- static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog";
+ private static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG =
+ "enable_letterbox_restart_confirmation_dialog";
- static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
+ private static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
"enable_letterbox_reachability_education";
+ private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG = true;
+
+ private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION = false;
+
+ /**
+ * The name of the {@link SharedPreferences} that holds which user has seen the Restart
+ * confirmation dialog.
+ */
+ private static final String DONT_SHOW_RESTART_DIALOG_PREF_NAME = "dont_show_restart_dialog";
+
+ /**
+ * The {@link SharedPreferences} instance for {@link #DONT_SHOW_RESTART_DIALOG_PREF_NAME}.
+ */
+ private final SharedPreferences mSharedPreferences;
+
// Whether the extended restart dialog is enabled
private boolean mIsRestartDialogEnabled;
@@ -64,12 +81,15 @@
mIsReachabilityEducationEnabled = context.getResources().getBoolean(
R.bool.config_letterboxIsReachabilityEducationEnabled);
mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false);
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
+ DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG);
mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
- false);
+ DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION);
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor,
this);
+ mSharedPreferences = context.getSharedPreferences(DONT_SHOW_RESTART_DIALOG_PREF_NAME,
+ Context.MODE_PRIVATE);
}
/**
@@ -102,18 +122,37 @@
mIsReachabilityEducationOverrideEnabled = enabled;
}
+ boolean getDontShowRestartDialogAgain(TaskInfo taskInfo) {
+ final int userId = taskInfo.userId;
+ final String packageName = taskInfo.topActivity.getPackageName();
+ return mSharedPreferences.getBoolean(
+ getDontShowAgainRestartKey(userId, packageName), /* default= */ false);
+ }
+
+ void setDontShowRestartDialogAgain(TaskInfo taskInfo) {
+ final int userId = taskInfo.userId;
+ final String packageName = taskInfo.topActivity.getPackageName();
+ mSharedPreferences.edit().putBoolean(getDontShowAgainRestartKey(userId, packageName),
+ true).apply();
+ }
+
@Override
public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
- // TODO(b/263349751): Update flag and default value to true
if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) {
mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
- false);
+ DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG);
}
+ // TODO(b/263349751): Update flag and default value to true
if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) {
mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_WINDOW_MANAGER,
- KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false);
+ KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
+ DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION);
}
}
-}
+
+ private String getDontShowAgainRestartKey(int userId, String packageName) {
+ return packageName + "@" + userId;
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 6627de5..3b2db51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -24,6 +24,7 @@
import android.hardware.display.DisplayManager;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import android.view.Display;
import android.view.InsetsSourceControl;
@@ -49,6 +50,7 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
@@ -91,6 +93,18 @@
private final SparseArray<CompatUIWindowManager> mActiveCompatLayouts = new SparseArray<>(0);
/**
+ * {@link SparseArray} that maps task ids to {@link RestartDialogWindowManager} that are
+ * currently visible
+ */
+ private final SparseArray<RestartDialogWindowManager> mTaskIdToRestartDialogWindowManagerMap =
+ new SparseArray<>(0);
+
+ /**
+ * {@link Set} of task ids for which we need to display a restart confirmation dialog
+ */
+ private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>();
+
+ /**
* The active Letterbox Education layout if there is one (there can be at most one active).
*
* <p>An active layout is a layout that is eligible to be shown for the associated task but
@@ -111,12 +125,15 @@
private final ShellExecutor mMainExecutor;
private final Lazy<Transitions> mTransitionsLazy;
private final DockStateReader mDockStateReader;
+ private final CompatUIConfiguration mCompatUIConfiguration;
private CompatUICallback mCallback;
// Only show each hint once automatically in the process life.
private final CompatUIHintsState mCompatUIHintsState;
+ private final CompatUIShellCommandHandler mCompatUIShellCommandHandler;
+
// Indicates if the keyguard is currently showing, in which case compat UIs shouldn't
// be shown.
private boolean mKeyguardShowing;
@@ -130,7 +147,9 @@
SyncTransactionQueue syncQueue,
ShellExecutor mainExecutor,
Lazy<Transitions> transitionsLazy,
- DockStateReader dockStateReader) {
+ DockStateReader dockStateReader,
+ CompatUIConfiguration compatUIConfiguration,
+ CompatUIShellCommandHandler compatUIShellCommandHandler) {
mContext = context;
mShellController = shellController;
mDisplayController = displayController;
@@ -140,14 +159,17 @@
mMainExecutor = mainExecutor;
mTransitionsLazy = transitionsLazy;
mCompatUIHintsState = new CompatUIHintsState();
- shellInit.addInitCallback(this::onInit, this);
mDockStateReader = dockStateReader;
+ mCompatUIConfiguration = compatUIConfiguration;
+ mCompatUIShellCommandHandler = compatUIShellCommandHandler;
+ shellInit.addInitCallback(this::onInit, this);
}
private void onInit() {
mShellController.addKeyguardChangeListener(this);
mDisplayController.addDisplayWindowListener(this);
mImeController.addPositionProcessor(this);
+ mCompatUIShellCommandHandler.onInit();
}
/** Sets the callback for UI interactions. */
@@ -164,6 +186,9 @@
*/
public void onCompatInfoChanged(TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
+ if (taskInfo != null && !taskInfo.topActivityInSizeCompat) {
+ mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
+ }
if (taskInfo.configuration == null || taskListener == null) {
// Null token means the current foreground activity is not in compatibility mode.
removeLayouts(taskInfo.taskId);
@@ -172,6 +197,7 @@
createOrUpdateCompatLayout(taskInfo, taskListener);
createOrUpdateLetterboxEduLayout(taskInfo, taskListener);
+ createOrUpdateRestartDialogLayout(taskInfo, taskListener);
}
@Override
@@ -278,7 +304,21 @@
ShellTaskOrganizer.TaskListener taskListener) {
return new CompatUIWindowManager(context,
taskInfo, mSyncQueue, mCallback, taskListener,
- mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState);
+ mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState,
+ mCompatUIConfiguration, this::onRestartButtonClicked);
+ }
+
+ private void onRestartButtonClicked(
+ Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState) {
+ if (mCompatUIConfiguration.isRestartDialogEnabled()
+ && !mCompatUIConfiguration.getDontShowRestartDialogAgain(
+ taskInfoState.first)) {
+ // We need to show the dialog
+ mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId);
+ onCompatInfoChanged(taskInfoState.first, taskInfoState.second);
+ } else {
+ mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
+ }
}
private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo,
@@ -327,6 +367,60 @@
mActiveLetterboxEduLayout = null;
}
+ private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo,
+ ShellTaskOrganizer.TaskListener taskListener) {
+ RestartDialogWindowManager layout =
+ mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
+ if (layout != null) {
+ // TODO(b/266262111) Handle theme change when taskListener changes
+ if (layout.getTaskListener() != taskListener) {
+ mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
+ }
+ layout.setRequestRestartDialog(
+ mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId));
+ // UI already exists, update the UI layout.
+ if (!layout.updateCompatInfo(taskInfo, taskListener,
+ showOnDisplay(layout.getDisplayId()))) {
+ // The layout is no longer eligible to be shown, remove from active layouts.
+ mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
+ }
+ return;
+ }
+ // Create a new UI layout.
+ final Context context = getOrCreateDisplayContext(taskInfo.displayId);
+ if (context == null) {
+ return;
+ }
+ layout = createRestartDialogWindowManager(context, taskInfo, taskListener);
+ layout.setRequestRestartDialog(
+ mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId));
+ if (layout.createLayout(showOnDisplay(taskInfo.displayId))) {
+ // The new layout is eligible to be shown, add it the active layouts.
+ mTaskIdToRestartDialogWindowManagerMap.put(taskInfo.taskId, layout);
+ }
+ }
+
+ @VisibleForTesting
+ RestartDialogWindowManager createRestartDialogWindowManager(Context context, TaskInfo taskInfo,
+ ShellTaskOrganizer.TaskListener taskListener) {
+ return new RestartDialogWindowManager(context, taskInfo, mSyncQueue, taskListener,
+ mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(),
+ this::onRestartDialogCallback, this::onRestartDialogDismissCallback,
+ mCompatUIConfiguration);
+ }
+
+ private void onRestartDialogCallback(
+ Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
+ mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId);
+ mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
+ }
+
+ private void onRestartDialogDismissCallback(
+ Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
+ mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId);
+ onCompatInfoChanged(stateInfo.first, stateInfo.second);
+ }
+
private void removeLayouts(int taskId) {
final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId);
if (layout != null) {
@@ -338,6 +432,14 @@
mActiveLetterboxEduLayout.release();
mActiveLetterboxEduLayout = null;
}
+
+ final RestartDialogWindowManager restartLayout =
+ mTaskIdToRestartDialogWindowManagerMap.get(taskId);
+ if (restartLayout != null) {
+ restartLayout.release();
+ mTaskIdToRestartDialogWindowManagerMap.remove(taskId);
+ mSetOfTaskIdsShowingRestartDialog.remove(taskId);
+ }
}
private Context getOrCreateDisplayContext(int displayId) {
@@ -382,6 +484,14 @@
if (mActiveLetterboxEduLayout != null && condition.test(mActiveLetterboxEduLayout)) {
callback.accept(mActiveLetterboxEduLayout);
}
+ for (int i = 0; i < mTaskIdToRestartDialogWindowManagerMap.size(); i++) {
+ final int taskId = mTaskIdToRestartDialogWindowManagerMap.keyAt(i);
+ final RestartDialogWindowManager layout =
+ mTaskIdToRestartDialogWindowManagerMap.get(taskId);
+ if (layout != null && condition.test(layout)) {
+ callback.accept(layout);
+ }
+ }
}
/** An implementation of {@link OnInsetsChangedListener} for a given display id. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index bce3ec4..fe95d04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -21,12 +21,14 @@
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.app.TaskInfo.CameraCompatControlState;
import android.content.Context;
import android.graphics.Rect;
import android.util.Log;
+import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
@@ -38,6 +40,8 @@
import com.android.wm.shell.compatui.CompatUIController.CompatUICallback;
import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
+import java.util.function.Consumer;
+
/**
* Window manager for the Size Compat restart button and Camera Compat control.
*/
@@ -50,6 +54,13 @@
private final CompatUICallback mCallback;
+ private final CompatUIConfiguration mCompatUIConfiguration;
+
+ private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
+
+ @NonNull
+ private TaskInfo mTaskInfo;
+
// Remember the last reported states in case visibility changes due to keyguard or IME updates.
@VisibleForTesting
boolean mHasSizeCompat;
@@ -68,12 +79,16 @@
CompatUIWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, CompatUICallback callback,
ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
- CompatUIHintsState compatUIHintsState) {
+ CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration,
+ Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
+ mTaskInfo = taskInfo;
mCallback = callback;
mHasSizeCompat = taskInfo.topActivityInSizeCompat;
mCameraCompatControlState = taskInfo.cameraCompatControlState;
mCompatUIHintsState = compatUIHintsState;
+ mCompatUIConfiguration = compatUIConfiguration;
+ mOnRestartButtonClicked = onRestartButtonClicked;
}
@Override
@@ -119,6 +134,7 @@
@Override
public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
boolean canShow) {
+ mTaskInfo = taskInfo;
final boolean prevHasSizeCompat = mHasSizeCompat;
final int prevCameraCompatControlState = mCameraCompatControlState;
mHasSizeCompat = taskInfo.topActivityInSizeCompat;
@@ -138,7 +154,7 @@
/** Called when the restart button is clicked. */
void onRestartButtonClicked() {
- mCallback.onSizeCompatRestartButtonClicked(mTaskId);
+ mOnRestartButtonClicked.accept(Pair.create(mTaskInfo, getTaskListener()));
}
/** Called when the camera treatment button is clicked. */
@@ -199,8 +215,14 @@
: taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
final int positionY = taskStableBounds.bottom - taskBounds.top
- mLayout.getMeasuredHeight();
-
+ // To secure a proper visualisation, we hide the layout while updating the position of
+ // the {@link SurfaceControl} it belongs.
+ final int oldVisibility = mLayout.getVisibility();
+ if (oldVisibility == View.VISIBLE) {
+ mLayout.setVisibility(View.GONE);
+ }
updateSurfacePosition(positionX, positionY);
+ mLayout.setVisibility(oldVisibility);
}
private void updateVisibilityOfViews() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index face243..db87f657 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -151,6 +151,7 @@
@Override
public void setConfiguration(Configuration configuration) {
super.setConfiguration(configuration);
+ // TODO(b/266262111): Investigate loss of theme configuration when switching TaskListener
mContext = mContext.createConfigurationContext(configuration);
}
@@ -169,6 +170,10 @@
initSurface(mLeash);
}
+ protected ShellTaskOrganizer.TaskListener getTaskListener() {
+ return mTaskListener;
+ }
+
/** Inits the z-order of the surface. */
private void initSurface(SurfaceControl leash) {
final int z = getZOrder();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
new file mode 100644
index 0000000..c53e638
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogLayout.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.wm.shell.R;
+
+import java.util.function.Consumer;
+
+/**
+ * Container for a SCM restart confirmation dialog and background dim.
+ */
+public class RestartDialogLayout extends ConstraintLayout implements DialogContainerSupplier {
+
+ private View mDialogContainer;
+ private TextView mDialogTitle;
+ private Drawable mBackgroundDim;
+
+ public RestartDialogLayout(Context context) {
+ this(context, null);
+ }
+
+ public RestartDialogLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RestartDialogLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RestartDialogLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public View getDialogContainerView() {
+ return mDialogContainer;
+ }
+
+ TextView getDialogTitle() {
+ return mDialogTitle;
+ }
+
+ @Override
+ public Drawable getBackgroundDimDrawable() {
+ return mBackgroundDim;
+ }
+
+ /**
+ * Register a callback for the dismiss button and background dim.
+ *
+ * @param callback The callback to register or null if all on click listeners should be removed.
+ */
+ void setDismissOnClickListener(@Nullable Runnable callback) {
+ final OnClickListener listener = callback == null ? null : view -> callback.run();
+ findViewById(R.id.letterbox_restart_dialog_dismiss_button).setOnClickListener(listener);
+ }
+
+ /**
+ * Register a callback for the restart button
+ *
+ * @param callback The callback to register or null if all on click listeners should be removed.
+ */
+ void setRestartOnClickListener(@Nullable Consumer<Boolean> callback) {
+ final CheckBox dontShowAgainCheckbox = findViewById(R.id.letterbox_restart_dialog_checkbox);
+ final OnClickListener listener = callback == null ? null : view -> callback.accept(
+ dontShowAgainCheckbox.isChecked());
+ findViewById(R.id.letterbox_restart_dialog_restart_button).setOnClickListener(listener);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mDialogContainer = findViewById(R.id.letterbox_restart_dialog_container);
+ mDialogTitle = findViewById(R.id.letterbox_restart_dialog_title);
+ mBackgroundDim = getBackground().mutate();
+ // Set the alpha of the background dim to 0 for enter animation.
+ mBackgroundDim.setAlpha(0);
+ // We add a no-op on-click listener to the dialog container so that clicks on it won't
+ // propagate to the listener of the layout (which represents the background dim).
+ mDialogContainer.setOnClickListener(view -> {});
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
new file mode 100644
index 0000000..10f25d0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui;
+
+import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.graphics.Rect;
+import android.provider.Settings;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.function.Consumer;
+
+/**
+ * Window manager for the Restart Dialog.
+ *
+ * TODO(b/263484314): Create abstraction of RestartDialogWindowManager and LetterboxEduWindowManager
+ */
+class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
+
+ /**
+ * The restart dialog should be the topmost child of the Task in case there can be more
+ * than one child.
+ */
+ private static final int Z_ORDER = Integer.MAX_VALUE;
+
+ private final DialogAnimationController<RestartDialogLayout> mAnimationController;
+
+ private final Transitions mTransitions;
+
+ // Remember the last reported state in case visibility changes due to keyguard or IME updates.
+ private boolean mRequestRestartDialog;
+
+ private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback;
+
+ private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartCallback;
+
+ private final CompatUIConfiguration mCompatUIConfiguration;
+
+ /**
+ * The vertical margin between the dialog container and the task stable bounds (excluding
+ * insets).
+ */
+ private final int mDialogVerticalMargin;
+
+ @NonNull
+ private TaskInfo mTaskInfo;
+
+ @Nullable
+ @VisibleForTesting
+ RestartDialogLayout mLayout;
+
+ RestartDialogWindowManager(Context context, TaskInfo taskInfo,
+ SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
+ DisplayLayout displayLayout, Transitions transitions,
+ Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback,
+ Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback,
+ CompatUIConfiguration compatUIConfiguration) {
+ this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
+ onRestartCallback, onDismissCallback,
+ new DialogAnimationController<>(context, "RestartDialogWindowManager"),
+ compatUIConfiguration);
+ }
+
+ @VisibleForTesting
+ RestartDialogWindowManager(Context context, TaskInfo taskInfo,
+ SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
+ DisplayLayout displayLayout, Transitions transitions,
+ Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback,
+ Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback,
+ DialogAnimationController<RestartDialogLayout> animationController,
+ CompatUIConfiguration compatUIConfiguration) {
+ super(context, taskInfo, syncQueue, taskListener, displayLayout);
+ mTaskInfo = taskInfo;
+ mTransitions = transitions;
+ mOnDismissCallback = onDismissCallback;
+ mOnRestartCallback = onRestartCallback;
+ mAnimationController = animationController;
+ mDialogVerticalMargin = (int) mContext.getResources().getDimension(
+ R.dimen.letterbox_restart_dialog_margin);
+ mCompatUIConfiguration = compatUIConfiguration;
+ }
+
+ @Override
+ protected int getZOrder() {
+ return Z_ORDER;
+ }
+
+ @Override
+ @Nullable
+ protected View getLayout() {
+ return mLayout;
+ }
+
+ @Override
+ protected void removeLayout() {
+ mLayout = null;
+ }
+
+ @Override
+ protected boolean eligibleToShowLayout() {
+ // We don't show this dialog if the user has explicitly selected so clicking on a checkbox.
+ return mRequestRestartDialog && !isTaskbarEduShowing() && (mLayout != null
+ || !mCompatUIConfiguration.getDontShowRestartDialogAgain(mTaskInfo));
+ }
+
+ @Override
+ protected View createLayout() {
+ mLayout = inflateLayout();
+ updateDialogMargins();
+
+ // startEnterAnimation will be called immediately if shell-transitions are disabled.
+ mTransitions.runOnIdle(this::startEnterAnimation);
+
+ return mLayout;
+ }
+
+ void setRequestRestartDialog(boolean enabled) {
+ mRequestRestartDialog = enabled;
+ }
+
+ @Override
+ public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
+ boolean canShow) {
+ mTaskInfo = taskInfo;
+ return super.updateCompatInfo(taskInfo, taskListener, canShow);
+ }
+
+ private void updateDialogMargins() {
+ if (mLayout == null) {
+ return;
+ }
+ final View dialogContainer = mLayout.getDialogContainerView();
+ ViewGroup.MarginLayoutParams marginParams =
+ (ViewGroup.MarginLayoutParams) dialogContainer.getLayoutParams();
+
+ final Rect taskBounds = getTaskBounds();
+ final Rect taskStableBounds = getTaskStableBounds();
+
+ marginParams.topMargin = taskStableBounds.top - taskBounds.top + mDialogVerticalMargin;
+ marginParams.bottomMargin =
+ taskBounds.bottom - taskStableBounds.bottom + mDialogVerticalMargin;
+ dialogContainer.setLayoutParams(marginParams);
+ }
+
+ private RestartDialogLayout inflateLayout() {
+ return (RestartDialogLayout) LayoutInflater.from(mContext).inflate(
+ R.layout.letterbox_restart_dialog_layout, null);
+ }
+
+ private void startEnterAnimation() {
+ if (mLayout == null) {
+ // Dialog has already been released.
+ return;
+ }
+ mAnimationController.startEnterAnimation(mLayout, /* endCallback= */
+ this::onDialogEnterAnimationEnded);
+ }
+
+ private void onDialogEnterAnimationEnded() {
+ if (mLayout == null) {
+ // Dialog has already been released.
+ return;
+ }
+ mLayout.setDismissOnClickListener(this::onDismiss);
+ mLayout.setRestartOnClickListener(dontShowAgain -> {
+ if (mLayout != null) {
+ mLayout.setDismissOnClickListener(null);
+ mAnimationController.startExitAnimation(mLayout, () -> {
+ release();
+ });
+ }
+ if (dontShowAgain) {
+ mCompatUIConfiguration.setDontShowRestartDialogAgain(mTaskInfo);
+ }
+ mOnRestartCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+ });
+ // Focus on the dialog title for accessibility.
+ mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
+
+ private void onDismiss() {
+ if (mLayout == null) {
+ return;
+ }
+
+ mLayout.setDismissOnClickListener(null);
+ mAnimationController.startExitAnimation(mLayout, () -> {
+ release();
+ mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener()));
+ });
+ }
+
+ @Override
+ public void release() {
+ mAnimationController.cancelAnimation();
+ super.release();
+ }
+
+ @Override
+ protected void onParentBoundsChanged() {
+ if (mLayout == null) {
+ return;
+ }
+ // Both the layout dimensions and dialog margins depend on the parent bounds.
+ WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams();
+ mLayout.setLayoutParams(windowLayoutParams);
+ updateDialogMargins();
+ relayout(windowLayoutParams);
+ }
+
+ @Override
+ protected void updateSurfacePosition() {
+ // Nothing to do, since the position of the surface is fixed to the top left corner (0,0)
+ // of the task (parent surface), which is the default position of a surface.
+ }
+
+ @Override
+ protected WindowManager.LayoutParams getWindowLayoutParams() {
+ final Rect taskBounds = getTaskBounds();
+ return getWindowLayoutParams(/* width= */ taskBounds.width(), /* height= */
+ taskBounds.height());
+ }
+
+ @VisibleForTesting
+ boolean isTaskbarEduShowing() {
+ return Settings.Secure.getInt(mContext.getContentResolver(),
+ LAUNCHER_TASKBAR_EDUCATION_SHOWING, /* def= */ 0) == 1;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 09f5cf1..25c430c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -56,7 +56,9 @@
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.compatui.CompatUIConfiguration;
import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.compatui.CompatUIShellCommandHandler;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.desktopmode.DesktopModeController;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
@@ -196,10 +198,11 @@
DisplayController displayController, DisplayInsetsController displayInsetsController,
DisplayImeController imeController, SyncTransactionQueue syncQueue,
@ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy,
- DockStateReader dockStateReader) {
+ DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration,
+ CompatUIShellCommandHandler compatUIShellCommandHandler) {
return new CompatUIController(context, shellInit, shellController, displayController,
displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy,
- dockStateReader);
+ dockStateReader, compatUIConfiguration, compatUIShellCommandHandler);
}
@WMSingleton
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 d3b9fa5..512a4ef 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
@@ -49,6 +49,7 @@
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.draganddrop.DragAndDropController;
@@ -93,6 +94,7 @@
import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition;
import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
+import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -192,7 +194,8 @@
SyncTransactionQueue syncQueue,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController) {
- return new DesktopModeWindowDecorViewModel(
+ if (DesktopModeStatus.isAnyEnabled()) {
+ return new DesktopModeWindowDecorViewModel(
context,
mainHandler,
mainChoreographer,
@@ -201,6 +204,14 @@
syncQueue,
desktopModeController,
desktopTasksController);
+ }
+ return new CaptionWindowDecorViewModel(
+ context,
+ mainHandler,
+ mainChoreographer,
+ taskOrganizer,
+ displayController,
+ syncQueue);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index f5f3573..a839a23 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -36,6 +36,7 @@
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArraySet;
@@ -251,7 +252,8 @@
* Show apps on desktop
*/
void showDesktopApps() {
- WindowContainerTransaction wct = bringDesktopAppsToFront();
+ // Bring apps to front, ignoring their visibility status to always ensure they are on top.
+ WindowContainerTransaction wct = bringDesktopAppsToFront(true /* ignoreVisibility */);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */);
@@ -260,8 +262,13 @@
}
}
+ /** Get number of tasks that are marked as visible */
+ int getVisibleTaskCount() {
+ return mDesktopModeTaskRepository.getVisibleTaskCount();
+ }
+
@NonNull
- private WindowContainerTransaction bringDesktopAppsToFront() {
+ private WindowContainerTransaction bringDesktopAppsToFront(boolean force) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
@@ -278,12 +285,14 @@
return wct;
}
- final boolean allActiveTasksAreVisible = taskInfos.stream()
- .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId));
- if (allActiveTasksAreVisible) {
- ProtoLog.d(WM_SHELL_DESKTOP_MODE,
- "bringDesktopAppsToFront: active tasks are already in front, skipping.");
- return wct;
+ if (!force) {
+ final boolean allActiveTasksAreVisible = taskInfos.stream()
+ .allMatch(info -> mDesktopModeTaskRepository.isVisibleTask(info.taskId));
+ if (allActiveTasksAreVisible) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "bringDesktopAppsToFront: active tasks are already in front, skipping.");
+ return wct;
+ }
}
ProtoLog.d(WM_SHELL_DESKTOP_MODE,
"bringDesktopAppsToFront: reordering all active tasks to the front");
@@ -354,7 +363,7 @@
if (wct == null) {
wct = new WindowContainerTransaction();
}
- wct.merge(bringDesktopAppsToFront(), true /* transfer */);
+ wct.merge(bringDesktopAppsToFront(false /* ignoreVisibility */), true /* transfer */);
wct.reorder(request.getTriggerTask().token, true /* onTop */);
return wct;
@@ -435,5 +444,15 @@
executeRemoteCallWithTaskPermission(mController, "showDesktopApps",
DesktopModeController::showDesktopApps);
}
+
+ @Override
+ public int getVisibleTaskCount() throws RemoteException {
+ int[] result = new int[1];
+ executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount",
+ controller -> result[0] = controller.getVisibleTaskCount(),
+ true /* blocking */
+ );
+ return result[0];
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 600ccc1..47342c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -143,6 +143,13 @@
}
/**
+ * Get number of tasks that are marked as visible
+ */
+ fun getVisibleTaskCount(): Int {
+ return visibleTasks.size
+ }
+
+ /**
* Add (or move if it already exists) the task to the top of the ordered list.
*/
fun addOrMoveFreeformTaskToTop(taskId: Int) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 3341470..2303325 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -84,8 +84,7 @@
fun showDesktopApps() {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps")
val wct = WindowContainerTransaction()
-
- bringDesktopAppsToFront(wct)
+ bringDesktopAppsToFront(wct, force = true)
// Execute transaction if there are pending operations
if (!wct.isEmpty) {
@@ -97,6 +96,11 @@
}
}
+ /** Get number of tasks that are marked as visible */
+ fun getVisibleTaskCount(): Int {
+ return desktopModeTaskRepository.getVisibleTaskCount()
+ }
+
/** Move a task with given `taskId` to desktop */
fun moveToDesktop(taskId: Int) {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) }
@@ -150,11 +154,11 @@
?: WINDOWING_MODE_UNDEFINED
}
- private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) {
+ private fun bringDesktopAppsToFront(wct: WindowContainerTransaction, force: Boolean = false) {
val activeTasks = desktopModeTaskRepository.getActiveTasks()
// Skip if all tasks are already visible
- if (activeTasks.isNotEmpty() && activeTasks.all(desktopModeTaskRepository::isVisibleTask)) {
+ if (!force && activeTasks.all(desktopModeTaskRepository::isVisibleTask)) {
ProtoLog.d(
WM_SHELL_DESKTOP_MODE,
"bringDesktopAppsToFront: active tasks are already in front, skipping."
@@ -310,5 +314,16 @@
Consumer(DesktopTasksController::showDesktopApps)
)
}
+
+ override fun getVisibleTaskCount(): Int {
+ val result = IntArray(1)
+ ExecutorUtils.executeRemoteCallWithTaskPermission(
+ controller,
+ "getVisibleTaskCount",
+ { controller -> result[0] = controller.getVisibleTaskCount() },
+ true /* blocking */
+ )
+ return result[0]
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index 5042bd6..d0739e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -23,4 +23,7 @@
/** Show apps on the desktop */
void showDesktopApps();
+
+ /** Get count of visible desktop tasks */
+ int getVisibleTaskCount();
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index b9caf62..26f47fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -114,7 +114,9 @@
t.setPosition(leash, positionInParent.x, positionInParent.y);
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
- t.show(leash);
+ if (taskInfo.isVisible) {
+ t.show(leash);
+ }
});
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
index cd61dbb..f6d67d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java
@@ -47,7 +47,7 @@
private final @NonNull PipBoundsState mPipBoundsState;
private final PipSnapAlgorithm mSnapAlgorithm;
- private final PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+ private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private float mDefaultSizePercent;
private float mMinAspectRatioForMinSize;
@@ -62,7 +62,7 @@
public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm,
- @NonNull PipKeepClearAlgorithm pipKeepClearAlgorithm) {
+ @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm) {
mPipBoundsState = pipBoundsState;
mSnapAlgorithm = pipSnapAlgorithm;
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index 7096a64..283b1ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.pip;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -41,6 +42,11 @@
}
}
+ @Nullable
+ public SurfaceControl getLeash() {
+ return mLeash;
+ }
+
/**
* Animates the internal {@link #mLeash} by a given fraction.
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
similarity index 97%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
index e3495e1..5045cf9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipKeepClearAlgorithmInterface.java
@@ -24,7 +24,7 @@
* Interface for interacting with keep clear algorithm used to move PiP window out of the way of
* keep clear areas.
*/
-public interface PipKeepClearAlgorithm {
+public interface PipKeepClearAlgorithmInterface {
/**
* Adjust the position of picture in picture window based on the registered keep clear areas.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index e6c7e10..83158ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -662,8 +662,8 @@
}
// Please file a bug to handle the unexpected transition type.
- throw new IllegalStateException("Entering PIP with unexpected transition type="
- + transitTypeToString(transitType));
+ android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type="
+ + transitTypeToString(transitType), new Throwable());
}
return false;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index 690505e..ed8dc7de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -26,14 +26,14 @@
import com.android.wm.shell.R;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import java.util.Set;
/**
* Calculates the adjusted position that does not occlude keep clear areas.
*/
-public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithm {
+public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterface {
private boolean mKeepClearAreaGravityEnabled =
SystemProperties.getBoolean(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 281ea53..431bd7b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -333,6 +333,9 @@
mTmpDestinationRectF.set(destinationBounds);
mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
final SurfaceControl surfaceControl = getSurfaceControl();
+ if (surfaceControl == null) {
+ return;
+ }
final SurfaceControl.Transaction menuTx =
mSurfaceControlTransactionFactory.getTransaction();
menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
@@ -359,6 +362,9 @@
}
final SurfaceControl surfaceControl = getSurfaceControl();
+ if (surfaceControl == null) {
+ return;
+ }
final SurfaceControl.Transaction menuTx =
mSurfaceControlTransactionFactory.getTransaction();
menuTx.setCrop(surfaceControl, destinationBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 01d81ff..525beb1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -46,8 +46,6 @@
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.os.UserManager;
import android.util.Pair;
import android.util.Size;
import android.view.DisplayInfo;
@@ -85,7 +83,7 @@
import com.android.wm.shell.pip.PipAppOpsListener;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipMediaController;
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSnapAlgorithm;
@@ -137,7 +135,7 @@
private PipAppOpsListener mAppOpsListener;
private PipMediaController mMediaController;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
- private PipKeepClearAlgorithm mPipKeepClearAlgorithm;
+ private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private PipBoundsState mPipBoundsState;
private PipMotionHelper mPipMotionHelper;
private PipTouchHandler mTouchHandler;
@@ -380,7 +378,7 @@
PipAnimationController pipAnimationController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
- PipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
PipBoundsState pipBoundsState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
@@ -419,7 +417,7 @@
PipAnimationController pipAnimationController,
PipAppOpsListener pipAppOpsListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
- PipKeepClearAlgorithm pipKeepClearAlgorithm,
+ PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
@NonNull PipBoundsState pipBoundsState,
PipMotionHelper pipMotionHelper,
PipMediaController pipMediaController,
@@ -618,7 +616,7 @@
return;
}
int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
- onDisplayChanged(
+ onDisplayChangedUncheck(
mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()),
false /* saveRestoreSnapFraction */);
int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
@@ -704,9 +702,12 @@
}
private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) {
- if (mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) {
- return;
+ if (!mPipBoundsState.getDisplayLayout().isSameGeometry(layout)) {
+ onDisplayChangedUncheck(layout, saveRestoreSnapFraction);
}
+ }
+
+ private void onDisplayChangedUncheck(DisplayLayout layout, boolean saveRestoreSnapFraction) {
Runnable updateDisplayLayout = () -> {
final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS
&& mPipBoundsState.getDisplayLayout().rotation() != layout.rotation();
@@ -814,7 +815,7 @@
@Override
public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
boolean animatingDismiss) {
- if (!mPipTaskOrganizer.isInPip()) {
+ if (!mPipTransitionState.hasEnteredPip()) {
return;
}
if (visible) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 83bc7c0..850c561 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -337,8 +337,10 @@
mMotionHelper.synchronizePinnedStackBounds();
reloadResources();
- // Recreate the dismiss target for the new orientation.
- mPipDismissTargetHandler.createOrUpdateDismissTarget();
+ if (mPipTaskOrganizer.isInPip()) {
+ // Recreate the dismiss target for the new orientation.
+ mPipDismissTargetHandler.createOrUpdateDismissTarget();
+ }
}
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index ce34d2f..1ff77f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -39,7 +39,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -65,7 +65,7 @@
@NonNull TvPipBoundsState tvPipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm) {
super(context, tvPipBoundsState, pipSnapAlgorithm,
- new PipKeepClearAlgorithm() {});
+ new PipKeepClearAlgorithmInterface() {});
this.mTvPipBoundsState = tvPipBoundsState;
this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
reloadResources(context);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 38099fc..21eeaa2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -535,17 +536,11 @@
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else {
- try {
- adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
- ActivityTaskManager.getService().startActivityFromRecents(taskId, options2);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error starting remote animation", e);
- }
+ taskId = INVALID_TASK_ID;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
"Cancel entering split as not supporting multi-instances");
Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
Toast.LENGTH_SHORT).show();
- return;
}
}
mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
@@ -586,17 +581,11 @@
fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else {
- try {
- adapter.getRunner().onAnimationCancelled(false /* isKeyguardOccluded */);
- pendingIntent1.send();
- } catch (RemoteException | PendingIntent.CanceledException e) {
- Slog.e(TAG, "Error starting remote animation", e);
- }
+ pendingIntent2 = null;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
"Cancel entering split as not supporting multi-instances");
Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text,
Toast.LENGTH_SHORT).show();
- return;
}
}
mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 8ddc3c04..39cf5f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -605,9 +605,19 @@
float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (options1 == null) options1 = new Bundle();
+ if (taskId2 == INVALID_TASK_ID) {
+ // Launching a solo task.
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+ options1 = activityOptions.toBundle();
+ addActivityOptions(options1, null /* launchTarget */);
+ wct.startTask(taskId1, options1);
+ mSyncQueue.queue(wct);
+ return;
+ }
+
addActivityOptions(options1, mSideStage);
wct.startTask(taskId1, options1);
-
startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter,
instanceId);
}
@@ -619,9 +629,19 @@
RemoteAnimationAdapter adapter, InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (options1 == null) options1 = new Bundle();
+ if (pendingIntent2 == null) {
+ // Launching a solo task.
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+ options1 = activityOptions.toBundle();
+ addActivityOptions(options1, null /* launchTarget */);
+ wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+ mSyncQueue.queue(wct);
+ return;
+ }
+
addActivityOptions(options1, mSideStage);
wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
-
startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, options2, splitPosition,
splitRatio, adapter, instanceId);
}
@@ -632,9 +652,19 @@
InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (options1 == null) options1 = new Bundle();
+ if (taskId == INVALID_TASK_ID) {
+ // Launching a solo task.
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+ options1 = activityOptions.toBundle();
+ addActivityOptions(options1, null /* launchTarget */);
+ wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
+ mSyncQueue.queue(wct);
+ return;
+ }
+
addActivityOptions(options1, mSideStage);
wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
-
startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter,
instanceId);
}
@@ -646,9 +676,19 @@
InstanceId instanceId) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (options1 == null) options1 = new Bundle();
+ if (taskId == INVALID_TASK_ID) {
+ // Launching a solo task.
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
+ options1 = activityOptions.toBundle();
+ addActivityOptions(options1, null /* launchTarget */);
+ wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
+ mSyncQueue.queue(wct);
+ return;
+ }
+
addActivityOptions(options1, mSideStage);
wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
-
startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter,
instanceId);
}
@@ -696,6 +736,34 @@
mShouldUpdateRecents = false;
mIsSplitEntering = true;
+ setSideStagePosition(sidePosition, wct);
+ if (!mMainStage.isActive()) {
+ mMainStage.activate(wct, false /* reparent */);
+ }
+
+ if (mainOptions == null) mainOptions = new Bundle();
+ addActivityOptions(mainOptions, mMainStage);
+ mainOptions = wrapAsSplitRemoteAnimation(adapter, mainOptions);
+
+ updateWindowBounds(mSplitLayout, wct);
+ if (mainTaskId == INVALID_TASK_ID) {
+ wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions);
+ } else {
+ wct.startTask(mainTaskId, mainOptions);
+ }
+
+ wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
+
+ mSyncQueue.queue(wct);
+ mSyncQueue.runInSync(t -> {
+ setDividerVisibility(true, t);
+ });
+
+ setEnterInstanceId(instanceId);
+ }
+
+ private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) {
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
if (isSplitScreenVisible()) {
mMainStage.evictAllChildren(evictWct);
@@ -739,37 +807,9 @@
};
RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
-
- if (mainOptions == null) {
- mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
- } else {
- ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
- mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
- mainOptions = mainActivityOptions.toBundle();
- }
-
- setSideStagePosition(sidePosition, wct);
- if (!mMainStage.isActive()) {
- mMainStage.activate(wct, false /* reparent */);
- }
-
- if (mainOptions == null) mainOptions = new Bundle();
- addActivityOptions(mainOptions, mMainStage);
- updateWindowBounds(mSplitLayout, wct);
- if (mainTaskId == INVALID_TASK_ID) {
- wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions);
- } else {
- wct.startTask(mainTaskId, mainOptions);
- }
- wct.reorder(mRootTaskInfo.token, true);
- wct.setForceTranslucent(mRootTaskInfo.token, false);
-
- mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t -> {
- setDividerVisibility(true, t);
- });
-
- setEnterInstanceId(instanceId);
+ ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+ activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+ return activityOptions.toBundle();
}
private void setEnterInstanceId(InstanceId instanceId) {
@@ -1052,6 +1092,7 @@
mSideStage.removeAllTasks(wct, false /* toTop */);
mMainStage.deactivate(wct, false /* toTop */);
wct.reorder(mRootTaskInfo.token, false /* onTop */);
+ wct.setForceTranslucent(mRootTaskInfo.token, true);
wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
onTransitionAnimationComplete();
} else {
@@ -1083,6 +1124,7 @@
mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */);
mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
+ finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
mSyncQueue.queue(finishedWCT);
mSyncQueue.runInSync(at -> {
@@ -1228,8 +1270,10 @@
return SPLIT_POSITION_UNDEFINED;
}
- private void addActivityOptions(Bundle opts, StageTaskListener stage) {
- opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
+ private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) {
+ if (launchTarget != null) {
+ opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token);
+ }
// Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
// will be canceled.
opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true);
@@ -1463,6 +1507,12 @@
}
private void onStageVisibilityChanged(StageListenerImpl stageListener) {
+ // If split didn't active, just ignore this callback because we should already did these
+ // on #applyExitSplitScreen.
+ if (!isSplitActive()) {
+ return;
+ }
+
final boolean sideStageVisible = mSideStageListener.mVisible;
final boolean mainStageVisible = mMainStageListener.mVisible;
@@ -1471,20 +1521,23 @@
return;
}
+ // Check if it needs to dismiss split screen when both stage invisible.
+ if (!mainStageVisible && mExitSplitScreenOnHide) {
+ exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
+ return;
+ }
+
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (!mainStageVisible) {
+ // Split entering background.
wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
true /* setReparentLeafTaskIfRelaunch */);
wct.setForceTranslucent(mRootTaskInfo.token, true);
- // Both stages are not visible, check if it needs to dismiss split screen.
- if (mExitSplitScreenOnHide) {
- exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
- }
} else {
wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
false /* setReparentLeafTaskIfRelaunch */);
- wct.setForceTranslucent(mRootTaskInfo.token, false);
}
+
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
setDividerVisibility(mainStageVisible, t);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
new file mode 100644
index 0000000..476a7ec
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -0,0 +1,271 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.os.Handler;
+import android.util.SparseArray;
+import android.view.Choreographer;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+
+/**
+ * View model for the window decoration with a caption and shadows. Works with
+ * {@link CaptionWindowDecoration}.
+ */
+public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
+ private final ShellTaskOrganizer mTaskOrganizer;
+ private final Context mContext;
+ private final Handler mMainHandler;
+ private final Choreographer mMainChoreographer;
+ private final DisplayController mDisplayController;
+ private final SyncTransactionQueue mSyncQueue;
+ private TaskOperations mTaskOperations;
+
+ private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
+
+ public CaptionWindowDecorViewModel(
+ Context context,
+ Handler mainHandler,
+ Choreographer mainChoreographer,
+ ShellTaskOrganizer taskOrganizer,
+ DisplayController displayController,
+ SyncTransactionQueue syncQueue) {
+ mContext = context;
+ mMainHandler = mainHandler;
+ mMainChoreographer = mainChoreographer;
+ mTaskOrganizer = taskOrganizer;
+ mDisplayController = displayController;
+ mSyncQueue = syncQueue;
+ }
+
+ @Override
+ public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
+ mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
+ }
+
+ @Override
+ public boolean onTaskOpening(
+ RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ if (!shouldShowWindowDecor(taskInfo)) return false;
+ createWindowDecoration(taskInfo, taskSurface, startT, finishT);
+ return true;
+ }
+
+ @Override
+ public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+ final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+
+ if (decoration == null) return;
+
+ decoration.relayout(taskInfo);
+ setupCaptionColor(taskInfo, decoration);
+ }
+
+ @Override
+ public void onTaskChanging(
+ RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+
+ if (!shouldShowWindowDecor(taskInfo)) {
+ if (decoration != null) {
+ destroyWindowDecoration(taskInfo);
+ }
+ return;
+ }
+
+ if (decoration == null) {
+ createWindowDecoration(taskInfo, taskSurface, startT, finishT);
+ } else {
+ decoration.relayout(taskInfo, startT, finishT);
+ }
+ }
+
+ @Override
+ public void onTaskClosing(
+ RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ if (decoration == null) return;
+
+ decoration.relayout(taskInfo, startT, finishT);
+ }
+
+ @Override
+ public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
+ final CaptionWindowDecoration decoration =
+ mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
+ if (decoration == null) return;
+
+ decoration.close();
+ }
+
+ private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
+ final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
+ decoration.setCaptionColor(statusBarColor);
+ }
+
+ private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
+ return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+ || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
+ && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode()
+ == WINDOWING_MODE_FREEFORM);
+ }
+
+ private void createWindowDecoration(
+ RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ final CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ if (oldDecoration != null) {
+ // close the old decoration if it exists to avoid two window decorations being added
+ oldDecoration.close();
+ }
+ final CaptionWindowDecoration windowDecoration =
+ new CaptionWindowDecoration(
+ mContext,
+ mDisplayController,
+ mTaskOrganizer,
+ taskInfo,
+ taskSurface,
+ mMainHandler,
+ mMainChoreographer,
+ mSyncQueue);
+ mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
+
+ final TaskPositioner taskPositioner =
+ new TaskPositioner(mTaskOrganizer, windowDecoration);
+ final CaptionTouchEventListener touchEventListener =
+ new CaptionTouchEventListener(taskInfo, taskPositioner);
+ windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
+ windowDecoration.setDragPositioningCallback(taskPositioner);
+ windowDecoration.setDragDetector(touchEventListener.mDragDetector);
+ windowDecoration.relayout(taskInfo, startT, finishT);
+ setupCaptionColor(taskInfo, windowDecoration);
+ }
+
+ private class CaptionTouchEventListener implements
+ View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
+
+ private final int mTaskId;
+ private final WindowContainerToken mTaskToken;
+ private final DragPositioningCallback mDragPositioningCallback;
+ private final DragDetector mDragDetector;
+
+ private int mDragPointerId = -1;
+
+ private CaptionTouchEventListener(
+ RunningTaskInfo taskInfo,
+ DragPositioningCallback dragPositioningCallback) {
+ mTaskId = taskInfo.taskId;
+ mTaskToken = taskInfo.token;
+ mDragPositioningCallback = dragPositioningCallback;
+ mDragDetector = new DragDetector(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ final int id = v.getId();
+ if (id == R.id.close_window) {
+ mTaskOperations.closeTask(mTaskToken);
+ } else if (id == R.id.back_button) {
+ mTaskOperations.injectBackKey();
+ } else if (id == R.id.minimize_window) {
+ mTaskOperations.minimizeTask(mTaskToken);
+ } else if (id == R.id.maximize_window) {
+ RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ mTaskOperations.maximizeTask(taskInfo);
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent e) {
+ if (v.getId() != R.id.caption) {
+ return false;
+ }
+ mDragDetector.onMotionEvent(e);
+
+ if (e.getAction() != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ if (taskInfo.isFocused) {
+ return false;
+ }
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(mTaskToken, true /* onTop */);
+ mSyncQueue.queue(wct);
+ return true;
+ }
+
+ /**
+ * @param e {@link MotionEvent} to process
+ * @return {@code true} if a drag is happening; or {@code false} if it is not
+ */
+ @Override
+ public boolean handleMotionEvent(MotionEvent e) {
+ final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ return false;
+ }
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mDragPointerId = e.getPointerId(0);
+ mDragPositioningCallback.onDragPositioningStart(
+ 0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ mDragPositioningCallback.onDragPositioningMove(
+ e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ mDragPositioningCallback.onDragPositioningEnd(
+ e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+ break;
+ }
+ }
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
new file mode 100644
index 0000000..060dc4e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -0,0 +1,231 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.VectorDrawable;
+import android.os.Handler;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
+ * {@link CaptionWindowDecorViewModel}. The caption bar contains a back button, minimize button,
+ * maximize button and close button.
+ */
+public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
+ private final Handler mHandler;
+ private final Choreographer mChoreographer;
+ private final SyncTransactionQueue mSyncQueue;
+
+ private View.OnClickListener mOnCaptionButtonClickListener;
+ private View.OnTouchListener mOnCaptionTouchListener;
+ private DragPositioningCallback mDragPositioningCallback;
+ private DragResizeInputListener mDragResizeListener;
+ private DragDetector mDragDetector;
+
+ private RelayoutParams mRelayoutParams = new RelayoutParams();
+ private final RelayoutResult<WindowDecorLinearLayout> mResult =
+ new RelayoutResult<>();
+
+ CaptionWindowDecoration(
+ Context context,
+ DisplayController displayController,
+ ShellTaskOrganizer taskOrganizer,
+ RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ Handler handler,
+ Choreographer choreographer,
+ SyncTransactionQueue syncQueue) {
+ super(context, displayController, taskOrganizer, taskInfo, taskSurface);
+
+ mHandler = handler;
+ mChoreographer = choreographer;
+ mSyncQueue = syncQueue;
+ }
+
+ void setCaptionListeners(
+ View.OnClickListener onCaptionButtonClickListener,
+ View.OnTouchListener onCaptionTouchListener) {
+ mOnCaptionButtonClickListener = onCaptionButtonClickListener;
+ mOnCaptionTouchListener = onCaptionTouchListener;
+ }
+
+ void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
+ mDragPositioningCallback = dragPositioningCallback;
+ }
+
+ void setDragDetector(DragDetector dragDetector) {
+ mDragDetector = dragDetector;
+ mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
+ }
+
+ @Override
+ void relayout(RunningTaskInfo taskInfo) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ relayout(taskInfo, t, t);
+ mSyncQueue.runInSync(transaction -> {
+ transaction.merge(t);
+ t.close();
+ });
+ }
+
+ void relayout(RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+ final int shadowRadiusID = taskInfo.isFocused
+ ? R.dimen.freeform_decor_shadow_focused_thickness
+ : R.dimen.freeform_decor_shadow_unfocused_thickness;
+ final boolean isFreeform =
+ taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
+
+ final WindowDecorLinearLayout oldRootView = mResult.mRootView;
+ final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+ final int outsetLeftId = R.dimen.freeform_resize_handle;
+ final int outsetTopId = R.dimen.freeform_resize_handle;
+ final int outsetRightId = R.dimen.freeform_resize_handle;
+ final int outsetBottomId = R.dimen.freeform_resize_handle;
+
+ mRelayoutParams.reset();
+ mRelayoutParams.mRunningTaskInfo = taskInfo;
+ mRelayoutParams.mLayoutResId = R.layout.caption_window_decor;
+ mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+ mRelayoutParams.mShadowRadiusId = shadowRadiusID;
+ if (isDragResizeable) {
+ mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+ }
+
+ relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
+ // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
+
+ mTaskOrganizer.applyTransaction(wct);
+
+ if (mResult.mRootView == null) {
+ // This means something blocks the window decor from showing, e.g. the task is hidden.
+ // Nothing is set up in this case including the decoration surface.
+ return;
+ }
+ if (oldRootView != mResult.mRootView) {
+ setupRootView();
+ }
+
+ if (!isDragResizeable) {
+ closeDragResizeListener();
+ return;
+ }
+
+ if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
+ closeDragResizeListener();
+ mDragResizeListener = new DragResizeInputListener(
+ mContext,
+ mHandler,
+ mChoreographer,
+ mDisplay.getDisplayId(),
+ mDecorationContainerSurface,
+ mDragPositioningCallback);
+ }
+
+ final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
+ .getScaledTouchSlop();
+ mDragDetector.setTouchSlop(touchSlop);
+
+ final int resize_handle = mResult.mRootView.getResources()
+ .getDimensionPixelSize(R.dimen.freeform_resize_handle);
+ final int resize_corner = mResult.mRootView.getResources()
+ .getDimensionPixelSize(R.dimen.freeform_resize_corner);
+ mDragResizeListener.setGeometry(
+ mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
+ }
+
+ /**
+ * Sets up listeners when a new root view is created.
+ */
+ private void setupRootView() {
+ final View caption = mResult.mRootView.findViewById(R.id.caption);
+ caption.setOnTouchListener(mOnCaptionTouchListener);
+ final View close = caption.findViewById(R.id.close_window);
+ close.setOnClickListener(mOnCaptionButtonClickListener);
+ final View back = caption.findViewById(R.id.back_button);
+ back.setOnClickListener(mOnCaptionButtonClickListener);
+ final View minimize = caption.findViewById(R.id.minimize_window);
+ minimize.setOnClickListener(mOnCaptionButtonClickListener);
+ final View maximize = caption.findViewById(R.id.maximize_window);
+ maximize.setOnClickListener(mOnCaptionButtonClickListener);
+ }
+
+ void setCaptionColor(int captionColor) {
+ if (mResult.mRootView == null) {
+ return;
+ }
+
+ final View caption = mResult.mRootView.findViewById(R.id.caption);
+ final GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground();
+ captionDrawable.setColor(captionColor);
+
+ final int buttonTintColorRes =
+ Color.valueOf(captionColor).luminance() < 0.5
+ ? R.color.decor_button_light_color
+ : R.color.decor_button_dark_color;
+ final ColorStateList buttonTintColor =
+ caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
+
+ final View back = caption.findViewById(R.id.back_button);
+ final VectorDrawable backBackground = (VectorDrawable) back.getBackground();
+ backBackground.setTintList(buttonTintColor);
+
+ final View minimize = caption.findViewById(R.id.minimize_window);
+ final VectorDrawable minimizeBackground = (VectorDrawable) minimize.getBackground();
+ minimizeBackground.setTintList(buttonTintColor);
+
+ final View maximize = caption.findViewById(R.id.maximize_window);
+ final VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground();
+ maximizeBackground.setTintList(buttonTintColor);
+
+ final View close = caption.findViewById(R.id.close_window);
+ final VectorDrawable closeBackground = (VectorDrawable) close.getBackground();
+ closeBackground.setTintList(buttonTintColor);
+ }
+
+ private void closeDragResizeListener() {
+ if (mDragResizeListener == null) {
+ return;
+ }
+ mDragResizeListener.close();
+ mDragResizeListener = null;
+ }
+
+ @Override
+ public void close() {
+ closeDragResizeListener();
+ super.close();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 00aab67..606cf28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -27,17 +27,12 @@
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.Looper;
-import android.os.SystemClock;
-import android.util.Log;
import android.util.SparseArray;
import android.view.Choreographer;
import android.view.InputChannel;
-import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -55,7 +50,6 @@
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
-import com.android.wm.shell.transition.Transitions;
import java.util.Optional;
@@ -74,9 +68,8 @@
private final Choreographer mMainChoreographer;
private final DisplayController mDisplayController;
private final SyncTransactionQueue mSyncQueue;
- private FreeformTaskTransitionStarter mTransitionStarter;
- private Optional<DesktopModeController> mDesktopModeController;
- private Optional<DesktopTasksController> mDesktopTasksController;
+ private final Optional<DesktopModeController> mDesktopModeController;
+ private final Optional<DesktopTasksController> mDesktopTasksController;
private boolean mTransitionDragActive;
private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -84,7 +77,8 @@
private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId =
new SparseArray<>();
private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
- private InputMonitorFactory mInputMonitorFactory;
+ private final InputMonitorFactory mInputMonitorFactory;
+ private TaskOperations mTaskOperations;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -136,7 +130,7 @@
@Override
public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
- mTransitionStarter = transitionStarter;
+ mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
}
@Override
@@ -204,47 +198,36 @@
if (decoration == null) return;
decoration.close();
- int displayId = taskInfo.displayId;
+ final int displayId = taskInfo.displayId;
if (mEventReceiversByDisplay.contains(displayId)) {
removeTaskFromEventReceiver(displayId);
}
}
- private class CaptionTouchEventListener implements
- View.OnClickListener, View.OnTouchListener {
+ private class DesktopModeTouchEventListener implements
+ View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
private final int mTaskId;
private final WindowContainerToken mTaskToken;
- private final DragResizeCallback mDragResizeCallback;
+ private final DragPositioningCallback mDragPositioningCallback;
private final DragDetector mDragDetector;
private int mDragPointerId = -1;
- private CaptionTouchEventListener(
+ private DesktopModeTouchEventListener(
RunningTaskInfo taskInfo,
- DragResizeCallback dragResizeCallback,
- DragDetector dragDetector) {
+ DragPositioningCallback dragPositioningCallback) {
mTaskId = taskInfo.taskId;
mTaskToken = taskInfo.token;
- mDragResizeCallback = dragResizeCallback;
- mDragDetector = dragDetector;
+ mDragPositioningCallback = dragPositioningCallback;
+ mDragDetector = new DragDetector(this);
}
@Override
public void onClick(View v) {
- DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
final int id = v.getId();
- if (id == R.id.close_window) {
- WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.removeTask(mTaskToken);
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- mTransitionStarter.startRemoveTransition(wct);
- } else {
- mSyncQueue.queue(wct);
- }
- } else if (id == R.id.back_button) {
- injectBackKey();
- } else if (id == R.id.caption_handle) {
+ if (id == R.id.caption_handle) {
decoration.createHandleMenu();
} else if (id == R.id.desktop_button) {
mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true));
@@ -258,44 +241,24 @@
}
}
- private void injectBackKey() {
- sendBackEvent(KeyEvent.ACTION_DOWN);
- sendBackEvent(KeyEvent.ACTION_UP);
- }
-
- private void sendBackEvent(int action) {
- final long when = SystemClock.uptimeMillis();
- final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK,
- 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD,
- 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
- InputDevice.SOURCE_KEYBOARD);
-
- ev.setDisplayId(mContext.getDisplay().getDisplayId());
- if (!InputManager.getInstance()
- .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
- Log.e(TAG, "Inject input event fail");
- }
- }
-
@Override
public boolean onTouch(View v, MotionEvent e) {
boolean isDrag = false;
- int id = v.getId();
+ final int id = v.getId();
if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) {
return false;
}
if (id == R.id.caption_handle) {
- isDrag = mDragDetector.detectDragEvent(e);
- handleEventForMove(e);
+ isDrag = mDragDetector.onMotionEvent(e);
}
if (e.getAction() != MotionEvent.ACTION_DOWN) {
return isDrag;
}
- RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
if (taskInfo.isFocused) {
return isDrag;
}
- WindowContainerTransaction wct = new WindowContainerTransaction();
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mTaskToken, true /* onTop */);
mSyncQueue.queue(wct);
return true;
@@ -303,39 +266,39 @@
/**
* @param e {@link MotionEvent} to process
- * @return {@code true} if a drag is happening; or {@code false} if it is not
+ * @return {@code true} if the motion event is handled.
*/
- private void handleEventForMove(MotionEvent e) {
- RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+ @Override
+ public boolean handleMotionEvent(MotionEvent e) {
+ final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
if (DesktopModeStatus.isProto2Enabled()
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
- return;
+ return false;
}
if (DesktopModeStatus.isProto1Enabled() && mDesktopModeController.isPresent()
- && mDesktopModeController.get().getDisplayAreaWindowingMode(
- taskInfo.displayId)
+ && mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId)
== WINDOWING_MODE_FULLSCREEN) {
- return;
+ return false;
}
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
mDragPointerId = e.getPointerId(0);
- mDragResizeCallback.onDragResizeStart(
+ mDragPositioningCallback.onDragPositioningStart(
0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
break;
}
case MotionEvent.ACTION_MOVE: {
- int dragPointerIdx = e.findPointerIndex(mDragPointerId);
- mDragResizeCallback.onDragResizeMove(
+ final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ mDragPositioningCallback.onDragPositioningMove(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
- int dragPointerIdx = e.findPointerIndex(mDragPointerId);
- int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId)
- .stableInsets().top;
- mDragResizeCallback.onDragResizeEnd(
+ final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ final int statusBarHeight = mDisplayController
+ .getDisplayLayout(taskInfo.displayId).stableInsets().top;
+ mDragPositioningCallback.onDragPositioningEnd(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
if (e.getRawY(dragPointerIdx) <= statusBarHeight) {
if (DesktopModeStatus.isProto2Enabled()) {
@@ -355,6 +318,7 @@
break;
}
}
+ return true;
}
}
@@ -408,7 +372,7 @@
*/
private void incrementEventReceiverTasks(int displayId) {
if (mEventReceiversByDisplay.contains(displayId)) {
- EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+ final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
eventReceiver.incrementTaskNumber();
} else {
createInputChannel(displayId);
@@ -418,7 +382,7 @@
// If all tasks on this display are gone, we don't need to monitor its input.
private void removeTaskFromEventReceiver(int displayId) {
if (!mEventReceiversByDisplay.contains(displayId)) return;
- EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
+ final EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
if (eventReceiver == null) return;
eventReceiver.decrementTaskNumber();
if (eventReceiver.getTasksOnDisplay() == 0) {
@@ -433,7 +397,7 @@
*/
private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
if (DesktopModeStatus.isProto2Enabled()) {
- DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+ final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null
|| focusedDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
handleCaptionThroughStatusBar(ev);
@@ -458,9 +422,9 @@
// If an UP/CANCEL action is received outside of caption bounds, turn off handle menu
private void handleEventOutsideFocusedCaption(MotionEvent ev) {
- int action = ev.getActionMasked();
+ final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+ final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null) {
return;
}
@@ -480,7 +444,7 @@
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
// Begin drag through status bar if applicable.
- DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+ final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor != null) {
boolean dragFromStatusBarAllowed = false;
if (DesktopModeStatus.isProto2Enabled()) {
@@ -499,14 +463,14 @@
break;
}
case MotionEvent.ACTION_UP: {
- DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+ final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null) {
mTransitionDragActive = false;
return;
}
if (mTransitionDragActive) {
mTransitionDragActive = false;
- int statusBarHeight = mDisplayController
+ final int statusBarHeight = mDisplayController
.getDisplayLayout(focusedDecor.mTaskInfo.displayId).stableInsets().top;
if (ev.getY() > statusBarHeight) {
if (DesktopModeStatus.isProto2Enabled()) {
@@ -530,10 +494,10 @@
@Nullable
private DesktopModeWindowDecoration getFocusedDecor() {
- int size = mWindowDecorByTaskId.size();
+ final int size = mWindowDecorByTaskId.size();
DesktopModeWindowDecoration focusedDecor = null;
for (int i = 0; i < size; i++) {
- DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
+ final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
if (decor != null && decor.isFocused()) {
focusedDecor = decor;
break;
@@ -543,16 +507,16 @@
}
private void createInputChannel(int displayId) {
- InputManager inputManager = InputManager.getInstance();
- InputMonitor inputMonitor =
+ final InputManager inputManager = InputManager.getInstance();
+ final InputMonitor inputMonitor =
mInputMonitorFactory.create(inputManager, mContext);
- EventReceiver eventReceiver = new EventReceiver(inputMonitor,
+ final EventReceiver eventReceiver = new EventReceiver(inputMonitor,
inputMonitor.getInputChannel(), Looper.myLooper());
mEventReceiversByDisplay.put(displayId, eventReceiver);
}
private void disposeInputChannel(int displayId) {
- EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId);
+ final EventReceiver eventReceiver = mEventReceiversByDisplay.removeReturnOld(displayId);
if (eventReceiver != null) {
eventReceiver.dispose();
}
@@ -571,7 +535,7 @@
SurfaceControl taskSurface,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ final DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (oldDecoration != null) {
// close the old decoration if it exists to avoid two window decorations being added
oldDecoration.close();
@@ -588,13 +552,13 @@
mSyncQueue);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
- TaskPositioner taskPositioner =
+ final TaskPositioner taskPositioner =
new TaskPositioner(mTaskOrganizer, windowDecoration, mDragStartListener);
- CaptionTouchEventListener touchEventListener =
- new CaptionTouchEventListener(
- taskInfo, taskPositioner, windowDecoration.getDragDetector());
+ final DesktopModeTouchEventListener touchEventListener =
+ new DesktopModeTouchEventListener(taskInfo, taskPositioner);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
- windowDecoration.setDragResizeCallback(taskPositioner);
+ windowDecoration.setDragPositioningCallback(taskPositioner);
+ windowDecoration.setDragDetector(touchEventListener.mDragDetector);
windowDecoration.relayout(taskInfo, startT, finishT);
incrementEventReceiverTasks(taskInfo.displayId);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 9c2beb9..744c18f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -25,7 +25,6 @@
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.PointF;
-import android.graphics.Rect;
import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
import android.view.Choreographer;
@@ -55,18 +54,16 @@
private View.OnClickListener mOnCaptionButtonClickListener;
private View.OnTouchListener mOnCaptionTouchListener;
- private DragResizeCallback mDragResizeCallback;
-
+ private DragPositioningCallback mDragPositioningCallback;
private DragResizeInputListener mDragResizeListener;
+ private DragDetector mDragDetector;
private RelayoutParams mRelayoutParams = new RelayoutParams();
+ private final int mCaptionMenuWidthId = R.dimen.freeform_decor_caption_menu_width;
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
private boolean mDesktopActive;
-
- private DragDetector mDragDetector;
-
private AdditionalWindow mHandleMenu;
DesktopModeWindowDecoration(
@@ -84,7 +81,6 @@
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mDesktopActive = DesktopModeStatus.isActive(mContext);
- mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
}
void setCaptionListeners(
@@ -94,12 +90,13 @@
mOnCaptionTouchListener = onCaptionTouchListener;
}
- void setDragResizeCallback(DragResizeCallback dragResizeCallback) {
- mDragResizeCallback = dragResizeCallback;
+ void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
+ mDragPositioningCallback = dragPositioningCallback;
}
- DragDetector getDragDetector() {
- return mDragDetector;
+ void setDragDetector(DragDetector dragDetector) {
+ mDragDetector = dragDetector;
+ mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
}
@Override
@@ -121,38 +118,26 @@
taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
- WindowDecorLinearLayout oldRootView = mResult.mRootView;
+ final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- int outsetLeftId = R.dimen.freeform_resize_handle;
- int outsetTopId = R.dimen.freeform_resize_handle;
- int outsetRightId = R.dimen.freeform_resize_handle;
- int outsetBottomId = R.dimen.freeform_resize_handle;
+ final int outsetLeftId = R.dimen.freeform_resize_handle;
+ final int outsetTopId = R.dimen.freeform_resize_handle;
+ final int outsetRightId = R.dimen.freeform_resize_handle;
+ final int outsetBottomId = R.dimen.freeform_resize_handle;
mRelayoutParams.reset();
mRelayoutParams.mRunningTaskInfo = taskInfo;
mRelayoutParams.mLayoutResId = R.layout.desktop_mode_window_decor;
mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
- mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
mRelayoutParams.mShadowRadiusId = shadowRadiusID;
if (isDragResizeable) {
mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
}
- final Resources resources = mDecorWindowContext.getResources();
- final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
- final int captionHeight = loadDimensionPixelSize(resources,
- mRelayoutParams.mCaptionHeightId);
- final int captionWidth = loadDimensionPixelSize(resources,
- mRelayoutParams.mCaptionWidthId);
- final int captionLeft = taskBounds.width() / 2
- - captionWidth / 2;
- final int captionTop = taskBounds.top
- <= captionHeight / 2 ? 0 : -captionHeight / 2;
- mRelayoutParams.setCaptionPosition(captionLeft, captionTop);
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
- taskInfo = null; // Clear it just in case we use it accidentally
+ // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
mTaskOrganizer.applyTransaction(wct);
@@ -194,15 +179,16 @@
mChoreographer,
mDisplay.getDisplayId(),
mDecorationContainerSurface,
- mDragResizeCallback);
+ mDragPositioningCallback);
}
- int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop();
+ final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
+ .getScaledTouchSlop();
mDragDetector.setTouchSlop(touchSlop);
- int resize_handle = mResult.mRootView.getResources()
+ final int resize_handle = mResult.mRootView.getResources()
.getDimensionPixelSize(R.dimen.freeform_resize_handle);
- int resize_corner = mResult.mRootView.getResources()
+ final int resize_corner = mResult.mRootView.getResources()
.getDimensionPixelSize(R.dimen.freeform_resize_corner);
mDragResizeListener.setGeometry(
mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
@@ -212,28 +198,22 @@
* Sets up listeners when a new root view is created.
*/
private void setupRootView() {
- View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+ final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
caption.setOnTouchListener(mOnCaptionTouchListener);
- View close = caption.findViewById(R.id.close_window);
- close.setOnClickListener(mOnCaptionButtonClickListener);
- View back = caption.findViewById(R.id.back_button);
- back.setOnClickListener(mOnCaptionButtonClickListener);
- View handle = caption.findViewById(R.id.caption_handle);
+ final View handle = caption.findViewById(R.id.caption_handle);
handle.setOnTouchListener(mOnCaptionTouchListener);
handle.setOnClickListener(mOnCaptionButtonClickListener);
updateButtonVisibility();
}
private void setupHandleMenu() {
- View menu = mHandleMenu.mWindowViewHost.getView();
- View fullscreen = menu.findViewById(R.id.fullscreen_button);
+ final View menu = mHandleMenu.mWindowViewHost.getView();
+ final View fullscreen = menu.findViewById(R.id.fullscreen_button);
fullscreen.setOnClickListener(mOnCaptionButtonClickListener);
- View desktop = menu.findViewById(R.id.desktop_button);
+ final View desktop = menu.findViewById(R.id.desktop_button);
desktop.setOnClickListener(mOnCaptionButtonClickListener);
- View split = menu.findViewById(R.id.split_screen_button);
+ final View split = menu.findViewById(R.id.split_screen_button);
split.setOnClickListener(mOnCaptionButtonClickListener);
- View more = menu.findViewById(R.id.more_button);
- more.setOnClickListener(mOnCaptionButtonClickListener);
}
/**
@@ -242,8 +222,8 @@
* @param visible whether or not the caption should be visible
*/
private void setCaptionVisibility(boolean visible) {
- int v = visible ? View.VISIBLE : View.GONE;
- View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+ final int v = visible ? View.VISIBLE : View.GONE;
+ final View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
captionView.setVisibility(v);
if (!visible) closeHandleMenu();
}
@@ -264,19 +244,15 @@
* Show or hide buttons
*/
void setButtonVisibility(boolean visible) {
- int visibility = visible ? View.VISIBLE : View.GONE;
- View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- View back = caption.findViewById(R.id.back_button);
- View close = caption.findViewById(R.id.close_window);
- back.setVisibility(visibility);
- close.setVisibility(visibility);
- int buttonTintColorRes =
+ final int visibility = visible ? View.VISIBLE : View.GONE;
+ final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+ final int buttonTintColorRes =
mDesktopActive ? R.color.decor_button_dark_color
: R.color.decor_button_light_color;
- ColorStateList buttonTintColor =
+ final ColorStateList buttonTintColor =
caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */);
- View handle = caption.findViewById(R.id.caption_handle);
- VectorDrawable handleBackground = (VectorDrawable) handle.getBackground();
+ final View handle = caption.findViewById(R.id.caption_handle);
+ final VectorDrawable handleBackground = (VectorDrawable) handle.getBackground();
handleBackground.setTintList(buttonTintColor);
caption.getBackground().setTint(visible ? Color.WHITE : Color.TRANSPARENT);
}
@@ -297,16 +273,20 @@
* Create and display handle menu window
*/
void createHandleMenu() {
- SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
final Resources resources = mDecorWindowContext.getResources();
- int x = mRelayoutParams.mCaptionX;
- int y = mRelayoutParams.mCaptionY;
- int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
- int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+ final int captionWidth = mTaskInfo.getConfiguration()
+ .windowConfiguration.getBounds().width();
+ final int menuWidth = loadDimensionPixelSize(
+ resources, mCaptionMenuWidthId);
+ final int height = loadDimensionPixelSize(
+ resources, mRelayoutParams.mCaptionHeightId);
+ final int x = mRelayoutParams.mCaptionX + (captionWidth / 2) - (menuWidth / 2)
+ - mResult.mDecorContainerOffsetX;
+ final int y = mRelayoutParams.mCaptionY - mResult.mDecorContainerOffsetY;
String namePrefix = "Caption Menu";
- mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t,
- x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY,
- width, height);
+ mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t, x, y,
+ menuWidth, height);
mSyncQueue.runInSync(transaction -> {
transaction.merge(t);
t.close();
@@ -353,8 +333,8 @@
* @return the point of the input in local space
*/
private PointF offsetCaptionLocation(MotionEvent ev) {
- PointF result = new PointF(ev.getX(), ev.getY());
- Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
+ final PointF result = new PointF(ev.getX(), ev.getY());
+ final Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
.positionInParent;
result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY);
result.offset(-positionInParent.x, -positionInParent.y);
@@ -370,9 +350,9 @@
*/
private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) {
if (mResult.mRootView == null) return false;
- PointF inputPoint = offsetCaptionLocation(ev);
- View view = mResult.mRootView.findViewById(layoutId);
- return view != null && view.pointInView(inputPoint.x, inputPoint.y, 0);
+ final PointF inputPoint = offsetCaptionLocation(ev);
+ final View view = mResult.mRootView.findViewById(layoutId);
+ return view != null && pointInView(view, inputPoint.x, inputPoint.y);
}
boolean checkTouchEventInHandle(MotionEvent ev) {
@@ -389,32 +369,39 @@
*/
void checkClickEvent(MotionEvent ev) {
if (mResult.mRootView == null) return;
- View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- PointF inputPoint = offsetCaptionLocation(ev);
if (!isHandleMenuActive()) {
- View handle = caption.findViewById(R.id.caption_handle);
- clickIfPointInView(inputPoint, handle);
+ final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+ final View handle = caption.findViewById(R.id.caption_handle);
+ clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle);
} else {
- View menu = mHandleMenu.mWindowViewHost.getView();
- View fullscreen = menu.findViewById(R.id.fullscreen_button);
+ final View menu = mHandleMenu.mWindowViewHost.getView();
+ final int captionWidth = mTaskInfo.getConfiguration().windowConfiguration
+ .getBounds().width();
+ final int menuX = mRelayoutParams.mCaptionX + (captionWidth / 2)
+ - (menu.getWidth() / 2);
+ final PointF inputPoint = new PointF(ev.getX() - menuX, ev.getY());
+ final View fullscreen = menu.findViewById(R.id.fullscreen_button);
if (clickIfPointInView(inputPoint, fullscreen)) return;
- View desktop = menu.findViewById(R.id.desktop_button);
+ final View desktop = menu.findViewById(R.id.desktop_button);
if (clickIfPointInView(inputPoint, desktop)) return;
- View split = menu.findViewById(R.id.split_screen_button);
+ final View split = menu.findViewById(R.id.split_screen_button);
if (clickIfPointInView(inputPoint, split)) return;
- View more = menu.findViewById(R.id.more_button);
- clickIfPointInView(inputPoint, more);
}
}
private boolean clickIfPointInView(PointF inputPoint, View v) {
- if (v.pointInView(inputPoint.x - v.getLeft(), inputPoint.y, 0)) {
+ if (pointInView(v, inputPoint.x, inputPoint.y)) {
mOnCaptionButtonClickListener.onClick(v);
return true;
}
return false;
}
+ private boolean pointInView(View v, float x, float y) {
+ return v != null && v.getLeft() <= x && v.getRight() >= x
+ && v.getTop() <= y && v.getBottom() >= y;
+ }
+
@Override
public void close() {
closeDragResizeListener();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
index 0abe8ab..4fac843 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor;
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
@@ -25,63 +26,82 @@
import android.view.MotionEvent;
/**
- * A detector for touch inputs that differentiates between drag and click inputs.
+ * A detector for touch inputs that differentiates between drag and click inputs. It receives a flow
+ * of {@link MotionEvent} and generates a new flow of motion events with slop in consideration to
+ * the event handler. In particular, it always passes down, up and cancel events. It'll pass move
+ * events only when there is at least one move event that's beyond the slop threshold. For the
+ * purpose of convenience it also passes all events of other actions.
+ *
* All touch events must be passed through this class to track a drag event.
*/
-public class DragDetector {
+class DragDetector {
+ private final MotionEventHandler mEventHandler;
+
+ private final PointF mInputDownPoint = new PointF();
private int mTouchSlop;
- private PointF mInputDownPoint;
private boolean mIsDragEvent;
private int mDragPointerId;
- public DragDetector(int touchSlop) {
- mTouchSlop = touchSlop;
- mInputDownPoint = new PointF();
- mIsDragEvent = false;
- mDragPointerId = -1;
+
+ private boolean mResultOfDownAction;
+
+ DragDetector(MotionEventHandler eventHandler) {
+ resetState();
+ mEventHandler = eventHandler;
}
/**
- * Determine if {@link MotionEvent} is part of a drag event.
- * @return {@code true} if this is a drag event, {@code false} if not
- */
- public boolean detectDragEvent(MotionEvent ev) {
- switch (ev.getAction()) {
+ * The receiver of the {@link MotionEvent} flow.
+ *
+ * @return the result returned by {@link #mEventHandler}, or the result when
+ * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
+ */
+ boolean onMotionEvent(MotionEvent ev) {
+ switch (ev.getActionMasked()) {
case ACTION_DOWN: {
+ // Only touch screens generate noisy moves.
+ mIsDragEvent = (ev.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN;
mDragPointerId = ev.getPointerId(0);
float rawX = ev.getRawX(0);
float rawY = ev.getRawY(0);
mInputDownPoint.set(rawX, rawY);
- return false;
+ mResultOfDownAction = mEventHandler.handleMotionEvent(ev);
+ return mResultOfDownAction;
}
case ACTION_MOVE: {
if (!mIsDragEvent) {
int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
- if (Math.hypot(dx, dy) > mTouchSlop) {
- mIsDragEvent = true;
- }
+ mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop;
}
- return mIsDragEvent;
+ if (mIsDragEvent) {
+ return mEventHandler.handleMotionEvent(ev);
+ } else {
+ return mResultOfDownAction;
+ }
}
- case ACTION_UP: {
- boolean result = mIsDragEvent;
- mIsDragEvent = false;
- mInputDownPoint.set(0, 0);
- mDragPointerId = -1;
- return result;
- }
+ case ACTION_UP:
case ACTION_CANCEL: {
- mIsDragEvent = false;
- mInputDownPoint.set(0, 0);
- mDragPointerId = -1;
- return false;
+ resetState();
+ return mEventHandler.handleMotionEvent(ev);
}
+ default:
+ return mEventHandler.handleMotionEvent(ev);
}
- return mIsDragEvent;
}
- public void setTouchSlop(int touchSlop) {
+ void setTouchSlop(int touchSlop) {
mTouchSlop = touchSlop;
}
+
+ private void resetState() {
+ mIsDragEvent = false;
+ mInputDownPoint.set(0, 0);
+ mDragPointerId = -1;
+ mResultOfDownAction = false;
+ }
+
+ interface MotionEventHandler {
+ boolean handleMotionEvent(MotionEvent ev);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
similarity index 76%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
index ee160a1..0191c60 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
@@ -19,28 +19,28 @@
/**
* Callback called when receiving drag-resize or drag-move related input events.
*/
-public interface DragResizeCallback {
+public interface DragPositioningCallback {
/**
- * Called when a drag resize starts.
+ * Called when a drag-resize or drag-move starts.
*
* @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use
* {@code 0} to indicate it's a move
- * @param x x coordinate in window decoration coordinate system where the drag resize starts
- * @param y y coordinate in window decoration coordinate system where the drag resize starts
+ * @param x x coordinate in window decoration coordinate system where the drag starts
+ * @param y y coordinate in window decoration coordinate system where the drag starts
*/
- void onDragResizeStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
+ void onDragPositioningStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
/**
- * Called when the pointer moves during a drag resize.
+ * Called when the pointer moves during a drag-resize or drag-move.
* @param x x coordinate in window decoration coordinate system of the new pointer location
* @param y y coordinate in window decoration coordinate system of the new pointer location
*/
- void onDragResizeMove(float x, float y);
+ void onDragPositioningMove(float x, float y);
/**
- * Called when a drag resize stops.
+ * Called when a drag-resize or drag-move stops.
* @param x x coordinate in window decoration coordinate system where the drag resize stops
* @param y y coordinate in window decoration coordinate system where the drag resize stops
*/
- void onDragResizeEnd(float x, float y);
+ void onDragPositioningEnd(float x, float y);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index d3f1332..7d954ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -48,7 +48,6 @@
* Task edges are for resizing with a mouse.
* Task corners are for resizing with touch input.
*/
-// TODO(b/251270585): investigate how to pass taps in corners to the tasks
class DragResizeInputListener implements AutoCloseable {
private static final String TAG = "DragResizeInputListener";
@@ -63,7 +62,7 @@
private final SurfaceControl mDecorationSurface;
private final InputChannel mInputChannel;
private final TaskResizeInputEventReceiver mInputEventReceiver;
- private final com.android.wm.shell.windowdecor.DragResizeCallback mCallback;
+ private final DragPositioningCallback mCallback;
private int mWidth;
private int mHeight;
@@ -84,7 +83,7 @@
Choreographer choreographer,
int displayId,
SurfaceControl decorationSurface,
- DragResizeCallback callback) {
+ DragPositioningCallback callback) {
mInputManager = context.getSystemService(InputManager.class);
mHandler = handler;
mChoreographer = choreographer;
@@ -115,7 +114,8 @@
mInputEventReceiver = new TaskResizeInputEventReceiver(
mInputChannel, mHandler, mChoreographer);
mCallback = callback;
- mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
+ mDragDetector = new DragDetector(mInputEventReceiver);
+ mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
}
/**
@@ -223,12 +223,12 @@
}
}
- private class TaskResizeInputEventReceiver extends InputEventReceiver {
+ private class TaskResizeInputEventReceiver extends InputEventReceiver
+ implements DragDetector.MotionEventHandler {
private final Choreographer mChoreographer;
private final Runnable mConsumeBatchEventRunnable;
private boolean mConsumeBatchEventScheduled;
private boolean mShouldHandleEvents;
- private boolean mDragging;
private TaskResizeInputEventReceiver(
InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -270,15 +270,15 @@
if (!(inputEvent instanceof MotionEvent)) {
return false;
}
+ return mDragDetector.onMotionEvent((MotionEvent) inputEvent);
+ }
- MotionEvent e = (MotionEvent) inputEvent;
+ @Override
+ public boolean handleMotionEvent(MotionEvent e) {
boolean result = false;
// Check if this is a touch event vs mouse event.
// Touch events are tracked in four corners. Other events are tracked in resize edges.
boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
- if (isTouch) {
- mDragging = mDragDetector.detectDragEvent(e);
- }
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
float x = e.getX(0);
@@ -293,7 +293,7 @@
float rawX = e.getRawX(0);
float rawY = e.getRawY(0);
int ctrlType = calculateCtrlType(isTouch, x, y);
- mCallback.onDragResizeStart(ctrlType, rawX, rawY);
+ mCallback.onDragPositioningStart(ctrlType, rawX, rawY);
result = true;
}
break;
@@ -305,24 +305,17 @@
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
float rawX = e.getRawX(dragPointerIndex);
float rawY = e.getRawY(dragPointerIndex);
- if (!isTouch) {
- // For all other types allow immediate dragging.
- mDragging = true;
- }
- if (mDragging) {
- mCallback.onDragResizeMove(rawX, rawY);
- result = true;
- }
+ mCallback.onDragPositioningMove(rawX, rawY);
+ result = true;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
- if (mShouldHandleEvents && mDragging) {
+ if (mShouldHandleEvents) {
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
- mCallback.onDragResizeEnd(
+ mCallback.onDragPositioningEnd(
e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
}
- mDragging = false;
mShouldHandleEvents = false;
mDragPointerId = -1;
result = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
new file mode 100644
index 0000000..aea3404
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
@@ -0,0 +1,112 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * Utility class to handle task operations performed on a window decoration.
+ */
+class TaskOperations {
+ private static final String TAG = "TaskOperations";
+
+ private final FreeformTaskTransitionStarter mTransitionStarter;
+ private final Context mContext;
+ private final SyncTransactionQueue mSyncQueue;
+
+ TaskOperations(FreeformTaskTransitionStarter transitionStarter, Context context,
+ SyncTransactionQueue syncQueue) {
+ mTransitionStarter = transitionStarter;
+ mContext = context;
+ mSyncQueue = syncQueue;
+ }
+
+ void injectBackKey() {
+ sendBackEvent(KeyEvent.ACTION_DOWN);
+ sendBackEvent(KeyEvent.ACTION_UP);
+ }
+
+ private void sendBackEvent(int action) {
+ final long when = SystemClock.uptimeMillis();
+ final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK,
+ 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD,
+ 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+ InputDevice.SOURCE_KEYBOARD);
+
+ ev.setDisplayId(mContext.getDisplay().getDisplayId());
+ if (!InputManager.getInstance()
+ .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
+ Log.e(TAG, "Inject input event fail");
+ }
+ }
+
+ void closeTask(WindowContainerToken taskToken) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.removeTask(taskToken);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitionStarter.startRemoveTransition(wct);
+ } else {
+ mSyncQueue.queue(wct);
+ }
+ }
+
+ void minimizeTask(WindowContainerToken taskToken) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(taskToken, false);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitionStarter.startMinimizedModeTransition(wct);
+ } else {
+ mSyncQueue.queue(wct);
+ }
+ }
+
+ void maximizeTask(RunningTaskInfo taskInfo) {
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
+ ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM;
+ int displayWindowingMode =
+ taskInfo.configuration.windowConfiguration.getDisplayWindowingMode();
+ wct.setWindowingMode(taskInfo.token,
+ targetWindowingMode == displayWindowingMode
+ ? WINDOWING_MODE_UNDEFINED : targetWindowingMode);
+ if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+ wct.setBounds(taskInfo.token, null);
+ }
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct);
+ } else {
+ mSyncQueue.queue(wct);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
index a49a300..d3f9227 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
@@ -23,7 +23,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
-class TaskPositioner implements DragResizeCallback {
+class TaskPositioner implements DragPositioningCallback {
@IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
@interface CtrlType {}
@@ -38,15 +38,17 @@
private final WindowDecoration mWindowDecoration;
private final Rect mTaskBoundsAtDragStart = new Rect();
- private final PointF mResizeStartPoint = new PointF();
- private final Rect mResizeTaskBounds = new Rect();
- // Whether the |dragResizing| hint should be sent with the next bounds change WCT.
- // Used to optimized fluid resizing of freeform tasks.
- private boolean mPendingDragResizeHint = false;
+ private final PointF mRepositionStartPoint = new PointF();
+ private final Rect mRepositionTaskBounds = new Rect();
+ private boolean mHasMoved = false;
private int mCtrlType;
private DragStartListener mDragStartListener;
+ TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) {
+ this(taskOrganizer, windowDecoration, dragStartListener -> {});
+ }
+
TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
DragStartListener dragStartListener) {
mTaskOrganizer = taskOrganizer;
@@ -55,72 +57,84 @@
}
@Override
- public void onDragResizeStart(int ctrlType, float x, float y) {
- if (ctrlType != CTRL_TYPE_UNDEFINED) {
- // The task is being resized, send the |dragResizing| hint to core with the first
- // bounds-change wct.
- mPendingDragResizeHint = true;
- }
+ public void onDragPositioningStart(int ctrlType, float x, float y) {
+ mHasMoved = false;
mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
mCtrlType = ctrlType;
mTaskBoundsAtDragStart.set(
mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
- mResizeStartPoint.set(x, y);
+ mRepositionStartPoint.set(x, y);
}
@Override
- public void onDragResizeMove(float x, float y) {
+ public void onDragPositioningMove(float x, float y) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (changeBounds(wct, x, y)) {
- if (mPendingDragResizeHint) {
+ // The task is being resized, send the |dragResizing| hint to core with the first
+ // bounds-change wct.
+ if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) {
// This is the first bounds change since drag resize operation started.
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
- mPendingDragResizeHint = false;
}
mTaskOrganizer.applyTransaction(wct);
+ mHasMoved = true;
}
}
@Override
- public void onDragResizeEnd(float x, float y) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
- changeBounds(wct, x, y);
- mTaskOrganizer.applyTransaction(wct);
+ public void onDragPositioningEnd(float x, float y) {
+ // |mHasMoved| being false means there is no real change to the task bounds in WM core, so
+ // we don't need a WCT to finish it.
+ if (mHasMoved) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
+ changeBounds(wct, x, y);
+ mTaskOrganizer.applyTransaction(wct);
+ }
- mCtrlType = 0;
+ mCtrlType = CTRL_TYPE_UNDEFINED;
mTaskBoundsAtDragStart.setEmpty();
- mResizeStartPoint.set(0, 0);
- mPendingDragResizeHint = false;
+ mRepositionStartPoint.set(0, 0);
+ mHasMoved = false;
}
private boolean changeBounds(WindowContainerTransaction wct, float x, float y) {
- float deltaX = x - mResizeStartPoint.x;
- mResizeTaskBounds.set(mTaskBoundsAtDragStart);
+ // |mRepositionTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not
+ // true, we can compare it against |mTaskBoundsAtDragStart|.
+ final int oldLeft = mHasMoved ? mRepositionTaskBounds.left : mTaskBoundsAtDragStart.left;
+ final int oldTop = mHasMoved ? mRepositionTaskBounds.top : mTaskBoundsAtDragStart.top;
+ final int oldRight = mHasMoved ? mRepositionTaskBounds.right : mTaskBoundsAtDragStart.right;
+ final int oldBottom =
+ mHasMoved ? mRepositionTaskBounds.bottom : mTaskBoundsAtDragStart.bottom;
+
+ final float deltaX = x - mRepositionStartPoint.x;
+ final float deltaY = y - mRepositionStartPoint.y;
+ mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
if ((mCtrlType & CTRL_TYPE_LEFT) != 0) {
- mResizeTaskBounds.left += deltaX;
+ mRepositionTaskBounds.left += deltaX;
}
if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) {
- mResizeTaskBounds.right += deltaX;
+ mRepositionTaskBounds.right += deltaX;
}
- float deltaY = y - mResizeStartPoint.y;
if ((mCtrlType & CTRL_TYPE_TOP) != 0) {
- mResizeTaskBounds.top += deltaY;
+ mRepositionTaskBounds.top += deltaY;
}
if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) {
- mResizeTaskBounds.bottom += deltaY;
+ mRepositionTaskBounds.bottom += deltaY;
}
- if (mCtrlType == 0) {
- mResizeTaskBounds.offset((int) deltaX, (int) deltaY);
+ if (mCtrlType == CTRL_TYPE_UNDEFINED) {
+ mRepositionTaskBounds.offset((int) deltaX, (int) deltaY);
}
- if (!mResizeTaskBounds.isEmpty()) {
- wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds);
- return true;
+ if (oldLeft == mRepositionTaskBounds.left && oldTop == mRepositionTaskBounds.top
+ && oldRight == mRepositionTaskBounds.right
+ && oldBottom == mRepositionTaskBounds.bottom) {
+ return false;
}
- return false;
+ wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ return true;
}
interface DragStartListener {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 7f85988..62b72f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -252,9 +252,7 @@
}
final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
- final int captionWidth = params.mCaptionWidthId == Resources.ID_NULL
- ? taskBounds.width()
- : loadDimensionPixelSize(resources, params.mCaptionWidthId);
+ final int captionWidth = taskBounds.width();
startT.setPosition(
mCaptionContainerSurface,
@@ -428,7 +426,6 @@
RunningTaskInfo mRunningTaskInfo;
int mLayoutResId;
int mCaptionHeightId;
- int mCaptionWidthId;
int mShadowRadiusId;
int mOutsetTopId;
@@ -454,7 +451,6 @@
void reset() {
mLayoutResId = Resources.ID_NULL;
mCaptionHeightId = Resources.ID_NULL;
- mCaptionWidthId = Resources.ID_NULL;
mShadowRadiusId = Resources.ID_NULL;
mOutsetTopId = Resources.ID_NULL;
diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
index 8949a75..27d40b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
@@ -17,7 +17,7 @@
<resources>
<!-- Resources used in WindowDecorationTests -->
<dimen name="test_freeform_decor_caption_height">32dp</dimen>
- <dimen name="test_freeform_decor_caption_width">216dp</dimen>
+ <dimen name="test_freeform_decor_caption_menu_width">216dp</dimen>
<dimen name="test_window_decor_left_outset">10dp</dimen>
<dimen name="test_window_decor_top_outset">20dp</dimen>
<dimen name="test_window_decor_right_outset">30dp</dimen>
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
index ff1d2990..d5bb901 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
@@ -28,9 +28,11 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -82,13 +84,14 @@
@Mock
SyncTransactionQueue mSyncQueue;
@Mock
- TaskViewTransitions mTaskViewTransitions;
+ Transitions mTransitions;
SurfaceSession mSession;
SurfaceControl mLeash;
Context mContext;
TaskView mTaskView;
+ TaskViewTransitions mTaskViewTransitions;
@Before
public void setUp() {
@@ -118,6 +121,10 @@
return null;
}).when(mSyncQueue).runInSync(any());
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ doReturn(true).when(mTransitions).isRegistered();
+ }
+ mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions));
mTaskView = new TaskView(mContext, mOrganizer, mTaskViewTransitions, mSyncQueue);
mTaskView.setListener(mExecutor, mViewListener);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 2e328b0..2754496 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -53,6 +53,7 @@
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.BackEvent;
+import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
import android.window.IBackNaviAnimationController;
import android.window.IOnBackInvokedCallback;
@@ -246,10 +247,11 @@
// Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+ ArgumentCaptor<BackMotionEvent> backEventCaptor =
+ ArgumentCaptor.forClass(BackMotionEvent.class);
verify(mIOnBackInvokedCallback).onBackStarted(backEventCaptor.capture());
assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget());
- verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
+ verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackMotionEvent.class));
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
@@ -271,17 +273,18 @@
RemoteAnimationTarget animationTarget = createAnimationTarget();
IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
- ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+ ArgumentCaptor<BackMotionEvent> backEventCaptor =
+ ArgumentCaptor.forClass(BackMotionEvent.class);
createNavigationInfo(animationTarget, null, null,
BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false);
triggerBackGesture();
- verify(appCallback, never()).onBackStarted(any(BackEvent.class));
+ verify(appCallback, never()).onBackStarted(any(BackMotionEvent.class));
verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
verify(appCallback, times(1)).onBackInvoked();
- verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
+ verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackMotionEvent.class));
verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
verify(mIOnBackInvokedCallback, never()).onBackInvoked();
}
@@ -314,7 +317,7 @@
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
}
@Test
@@ -333,7 +336,7 @@
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
}
@@ -349,7 +352,7 @@
// Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mIOnBackInvokedCallback).onBackStarted(any(BackMotionEvent.class));
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
index 3aefc3f..ba9c159 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import android.window.BackEvent;
+import android.window.BackMotionEvent;
import org.junit.Before;
import org.junit.Test;
@@ -38,7 +39,7 @@
@Test
public void generatesProgress_onStart() {
mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
- BackEvent event = mTouchTracker.createStartEvent(null);
+ BackMotionEvent event = mTouchTracker.createStartEvent(null);
assertEquals(event.getProgress(), 0f, 0f);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index e6711ac..8b025cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.bubbles;
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -32,6 +34,7 @@
import android.app.Notification;
import android.app.PendingIntent;
+import android.content.Intent;
import android.content.LocusId;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -94,6 +97,7 @@
private Bubble mBubbleInterruptive;
private Bubble mBubbleDismissed;
private Bubble mBubbleLocusId;
+ private Bubble mAppBubble;
private BubbleData mBubbleData;
private TestableBubblePositioner mPositioner;
@@ -178,6 +182,11 @@
mBubbleMetadataFlagListener,
mPendingIntentCanceledListener,
mMainExecutor);
+
+ Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
+ appBubbleIntent.setPackage(mContext.getPackageName());
+ mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mMainExecutor);
+
mPositioner = new TestableBubblePositioner(mContext,
mock(WindowManager.class));
mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner,
@@ -1089,6 +1098,18 @@
assertOverflowChangedTo(ImmutableList.of());
}
+ @Test
+ public void test_removeAppBubble_skipsOverflow() {
+ mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/,
+ false /* showInShade */);
+ assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble);
+
+ mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE);
+
+ assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull();
+ assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull();
+ }
+
private void verifyUpdateReceived() {
verify(mListener).applyUpdate(mUpdateCaptor.capture());
reset(mListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
index 1636c5f..0a31338 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -21,7 +21,6 @@
import android.util.SparseArray
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.bubbles.storage.BubbleXmlHelperTest.Companion.sparseArraysEqual
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertTrue
@@ -36,7 +35,8 @@
// user, package, shortcut, notification key, height, res-height, title, taskId, locusId
private val user0Bubbles = listOf(
- BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null),
+ BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1, null,
+ true),
BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
@@ -44,7 +44,8 @@
)
private val user1Bubbles = listOf(
- BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null),
+ BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3, null,
+ true),
BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
null),
BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
@@ -76,6 +77,6 @@
assertEquals(actual.size(), 0)
repository.persistsToDisk(bubbles)
- assertTrue(sparseArraysEqual(bubbles, repository.readFromDisk()))
+ assertTrue(bubbles.contentEquals(repository.readFromDisk()))
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
index 4ab9f87..3bfbcd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleXmlHelperTest.kt
@@ -34,7 +34,8 @@
class BubbleXmlHelperTest : ShellTestCase() {
private val user0Bubbles = listOf(
- BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1),
+ BubbleEntity(0, "com.example.messenger", "shortcut-1", "0k1", 120, 0, null, 1,
+ isDismissable = true),
BubbleEntity(10, "com.example.chat", "alice and bob", "0k2", 0, 16537428, "title", 2,
null),
BubbleEntity(0, "com.example.messenger", "shortcut-2", "0k3", 120, 0, null,
@@ -42,7 +43,8 @@
)
private val user1Bubbles = listOf(
- BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3),
+ BubbleEntity(1, "com.example.messenger", "shortcut-1", "1k1", 120, 0, null, 3,
+ isDismissable = true),
BubbleEntity(12, "com.example.chat", "alice and bob", "1k2", 0, 16537428, "title", 4,
null),
BubbleEntity(1, "com.example.messenger", "shortcut-2", "1k3", 120, 0, null,
@@ -51,28 +53,6 @@
private val bubbles = SparseArray<List<BubbleEntity>>()
- // Checks that the contents of the two sparse arrays are the same.
- companion object {
- fun sparseArraysEqual(
- one: SparseArray<List<BubbleEntity>>?,
- two: SparseArray<List<BubbleEntity>>?
- ): Boolean {
- if (one == null && two == null) return true
- if ((one == null) != (two == null)) return false
- if (one!!.size() != two!!.size()) return false
- for (i in 0 until one.size()) {
- val k1 = one.keyAt(i)
- val v1 = one.valueAt(i)
- val k2 = two.keyAt(i)
- val v2 = two.valueAt(i)
- if (k1 != k2 && v1 != v2) {
- return false
- }
- }
- return true
- }
- }
-
@Before
fun setup() {
bubbles.put(0, user0Bubbles)
@@ -83,14 +63,14 @@
fun testWriteXml() {
val expectedEntries = """
<bs uid="0">
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" />
-<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" />
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" d="true" />
+<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" d="false" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" d="false" />
</bs>
<bs uid="1">
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" />
-<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" />
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" d="true" />
+<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" d="false" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" d="false" />
</bs>
""".trimIndent()
ByteArrayOutputStream().use {
@@ -107,19 +87,19 @@
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<bs v="2">
<bs uid="0">
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" />
-<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" />
-<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="0k1" h="120" hid="0" tid="1" d="true" />
+<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="0k2" h="0" hid="16537428" t="title" tid="2" d="false" />
+<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="0k3" h="120" hid="0" tid="-1" l="l3" d="false" />
</bs>
<bs uid="1">
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" />
-<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" />
-<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-1" key="1k1" h="120" hid="0" tid="3" d="true" />
+<bb uid="12" pkg="com.example.chat" sid="alice and bob" key="1k2" h="0" hid="16537428" t="title" tid="4" d="false" />
+<bb uid="1" pkg="com.example.messenger" sid="shortcut-2" key="1k3" h="120" hid="0" tid="-1" l="l4" d="false" />
</bs>
</bs>
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
- assertTrue("failed parsing bubbles from xml\n$src", sparseArraysEqual(bubbles, actual))
+ assertTrue("failed parsing bubbles from xml\n$src", bubbles.contentEquals(actual))
}
// V0 -> V1 happened prior to release / during dogfood so nothing is saved
@@ -161,8 +141,7 @@
</bs>
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
- assertTrue("failed parsing bubbles from xml\n$src",
- sparseArraysEqual(expectedBubbles, actual))
+ assertTrue("failed parsing bubbles from xml\n$src", expectedBubbles.contentEquals(actual))
}
/**
@@ -187,7 +166,7 @@
""".trimIndent()
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertTrue("failed parsing bubbles from xml\n$src",
- sparseArraysEqual(expectedBubbles, actual))
+ expectedBubbles.contentEquals(actual))
}
@Test
@@ -210,6 +189,6 @@
)
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
assertTrue("failed parsing bubbles from xml\n$src",
- sparseArraysEqual(expectedBubbles, actual))
+ expectedBubbles.contentEquals(actual))
}
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 2fc0914..875e610 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -94,7 +94,10 @@
private @Mock Lazy<Transitions> mMockTransitionsLazy;
private @Mock CompatUIWindowManager mMockCompatLayout;
private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
+ private @Mock RestartDialogWindowManager mMockRestartDialogLayout;
private @Mock DockStateReader mDockStateReader;
+ private @Mock CompatUIConfiguration mCompatUIConfiguration;
+ private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler;
@Captor
ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -112,10 +115,17 @@
doReturn(TASK_ID).when(mMockLetterboxEduLayout).getTaskId();
doReturn(true).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
doReturn(true).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
+
+ doReturn(DISPLAY_ID).when(mMockRestartDialogLayout).getDisplayId();
+ doReturn(TASK_ID).when(mMockRestartDialogLayout).getTaskId();
+ doReturn(true).when(mMockRestartDialogLayout).createLayout(anyBoolean());
+ doReturn(true).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean());
+
mShellInit = spy(new ShellInit(mMockExecutor));
mController = new CompatUIController(mContext, mShellInit, mMockShellController,
mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
- mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader) {
+ mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader,
+ mCompatUIConfiguration, mCompatUIShellCommandHandler) {
@Override
CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
@@ -127,6 +137,12 @@
TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
return mMockLetterboxEduLayout;
}
+
+ @Override
+ RestartDialogWindowManager createRestartDialogWindowManager(Context context,
+ TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
+ return mMockRestartDialogLayout;
+ }
};
mShellInit.init();
spyOn(mController);
@@ -159,6 +175,8 @@
verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
eq(mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
// Verify that the compat controls and letterbox education are updated with new size compat
// info.
@@ -167,10 +185,12 @@
CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED);
mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
- verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- true);
- verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- true);
+ verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ true);
+ verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ true);
+ verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ true);
// Verify that compat controls and letterbox education are removed with null task listener.
clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
@@ -180,12 +200,14 @@
verify(mMockCompatLayout).release();
verify(mMockLetterboxEduLayout).release();
+ verify(mMockRestartDialogLayout).release();
}
@Test
public void testOnCompatInfoChanged_createLayoutReturnsFalse() {
doReturn(false).when(mMockCompatLayout).createLayout(anyBoolean());
doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
+ doReturn(false).when(mMockRestartDialogLayout).createLayout(anyBoolean());
TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
CAMERA_COMPAT_CONTROL_HIDDEN);
@@ -194,6 +216,8 @@
verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
eq(mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
// Verify that the layout is created again.
clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
@@ -201,15 +225,19 @@
verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
+ verify(mMockRestartDialogLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
eq(mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
}
@Test
public void testOnCompatInfoChanged_updateCompatInfoReturnsFalse() {
doReturn(false).when(mMockCompatLayout).updateCompatInfo(any(), any(), anyBoolean());
doReturn(false).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
+ doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean());
TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
CAMERA_COMPAT_CONTROL_HIDDEN);
@@ -218,24 +246,33 @@
verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
eq(mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
- clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
+ clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout,
+ mController);
mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
- verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- true);
- verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- true);
+ verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ true);
+ verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ true);
+ verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ true);
// Verify that the layout is created again.
- clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController);
+ clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout,
+ mController);
mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
+ verify(mMockRestartDialogLayout, never()).updateCompatInfo(any(), any(), anyBoolean());
verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo),
eq(mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
}
@@ -259,6 +296,7 @@
verify(mMockCompatLayout, never()).release();
verify(mMockLetterboxEduLayout, never()).release();
+ verify(mMockRestartDialogLayout, never()).release();
verify(mMockDisplayInsetsController, never()).removeInsetsChangedListener(eq(DISPLAY_ID),
any());
@@ -267,6 +305,7 @@
verify(mMockDisplayInsetsController).removeInsetsChangedListener(eq(DISPLAY_ID), any());
verify(mMockCompatLayout).release();
verify(mMockLetterboxEduLayout).release();
+ verify(mMockRestartDialogLayout).release();
}
@Test
@@ -278,11 +317,13 @@
verify(mMockCompatLayout, never()).updateDisplayLayout(any());
verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(any());
+ verify(mMockRestartDialogLayout, never()).updateDisplayLayout(any());
mController.onDisplayConfigurationChanged(DISPLAY_ID, new Configuration());
verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout);
verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout);
+ verify(mMockRestartDialogLayout).updateDisplayLayout(mMockDisplayLayout);
}
@Test
@@ -301,12 +342,14 @@
verify(mMockCompatLayout).updateDisplayLayout(mMockDisplayLayout);
verify(mMockLetterboxEduLayout).updateDisplayLayout(mMockDisplayLayout);
+ verify(mMockRestartDialogLayout).updateDisplayLayout(mMockDisplayLayout);
// No update if the insets state is the same.
- clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
+ clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout);
mOnInsetsChangedListenerCaptor.getValue().insetsChanged(new InsetsState(insetsState));
verify(mMockCompatLayout, never()).updateDisplayLayout(mMockDisplayLayout);
verify(mMockLetterboxEduLayout, never()).updateDisplayLayout(mMockDisplayLayout);
+ verify(mMockRestartDialogLayout, never()).updateDisplayLayout(mMockDisplayLayout);
}
@Test
@@ -319,22 +362,26 @@
verify(mMockCompatLayout).updateVisibility(false);
verify(mMockLetterboxEduLayout).updateVisibility(false);
+ verify(mMockRestartDialogLayout).updateVisibility(false);
// Verify button remains hidden while IME is showing.
TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
CAMERA_COMPAT_CONTROL_HIDDEN);
mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
- verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- false);
- verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- false);
+ verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ false);
+ verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ false);
+ verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ false);
// Verify button is shown after IME is hidden.
mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false);
verify(mMockCompatLayout).updateVisibility(true);
verify(mMockLetterboxEduLayout).updateVisibility(true);
+ verify(mMockRestartDialogLayout).updateVisibility(true);
}
@Test
@@ -347,22 +394,26 @@
verify(mMockCompatLayout).updateVisibility(false);
verify(mMockLetterboxEduLayout).updateVisibility(false);
+ verify(mMockRestartDialogLayout).updateVisibility(false);
// Verify button remains hidden while keyguard is showing.
TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
CAMERA_COMPAT_CONTROL_HIDDEN);
mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
- verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- false);
- verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */
- false);
+ verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ false);
+ verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ false);
+ verify(mMockRestartDialogLayout).updateCompatInfo(taskInfo, mMockTaskListener,
+ /* canShow= */ false);
// Verify button is shown after keyguard becomes not showing.
mController.onKeyguardVisibilityChanged(false, false, false);
verify(mMockCompatLayout).updateVisibility(true);
verify(mMockLetterboxEduLayout).updateVisibility(true);
+ verify(mMockRestartDialogLayout).updateVisibility(true);
}
@Test
@@ -375,20 +426,23 @@
verify(mMockCompatLayout, times(2)).updateVisibility(false);
verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false);
+ verify(mMockRestartDialogLayout, times(2)).updateVisibility(false);
- clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
+ clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout);
// Verify button remains hidden after keyguard becomes not showing since IME is showing.
mController.onKeyguardVisibilityChanged(false, false, false);
verify(mMockCompatLayout).updateVisibility(false);
verify(mMockLetterboxEduLayout).updateVisibility(false);
+ verify(mMockRestartDialogLayout).updateVisibility(false);
// Verify button is shown after IME is not showing.
mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false);
verify(mMockCompatLayout).updateVisibility(true);
verify(mMockLetterboxEduLayout).updateVisibility(true);
+ verify(mMockRestartDialogLayout).updateVisibility(true);
}
@Test
@@ -401,20 +455,23 @@
verify(mMockCompatLayout, times(2)).updateVisibility(false);
verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false);
+ verify(mMockRestartDialogLayout, times(2)).updateVisibility(false);
- clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout);
+ clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout);
// Verify button remains hidden after IME is hidden since keyguard is showing.
mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false);
verify(mMockCompatLayout).updateVisibility(false);
verify(mMockLetterboxEduLayout).updateVisibility(false);
+ verify(mMockRestartDialogLayout).updateVisibility(false);
// Verify button is shown after keyguard becomes not showing.
mController.onKeyguardVisibilityChanged(false, false, false);
verify(mMockCompatLayout).updateVisibility(true);
verify(mMockLetterboxEduLayout).updateVisibility(true);
+ verify(mMockRestartDialogLayout).updateVisibility(true);
}
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 7d3e718..5f294d5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -31,6 +31,7 @@
import android.app.TaskInfo;
import android.app.TaskInfo.CameraCompatControlState;
import android.testing.AndroidTestingRunner;
+import android.util.Pair;
import android.view.LayoutInflater;
import android.view.SurfaceControlViewHost;
import android.widget.ImageButton;
@@ -45,12 +46,17 @@
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
+import junit.framework.Assert;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.function.Consumer;
+
/**
* Tests for {@link CompatUILayout}.
*
@@ -65,20 +71,22 @@
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private CompatUIController.CompatUICallback mCallback;
+ @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock private SurfaceControlViewHost mViewHost;
+ @Mock private CompatUIConfiguration mCompatUIConfiguration;
private CompatUIWindowManager mWindowManager;
private CompatUILayout mLayout;
+ private TaskInfo mTaskInfo;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
-
- mWindowManager = new CompatUIWindowManager(mContext,
- createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN),
- mSyncTransactionQueue, mCallback, mTaskListener,
- new DisplayLayout(), new CompatUIHintsState());
+ mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+ mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
+ mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+ mCompatUIConfiguration, mOnRestartButtonClicked);
mLayout = (CompatUILayout)
LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, null);
@@ -95,8 +103,15 @@
final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button);
button.performClick();
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> restartCaptor =
+ ArgumentCaptor.forClass(Pair.class);
+
verify(mWindowManager).onRestartButtonClicked();
- verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
+ verify(mOnRestartButtonClicked).accept(restartCaptor.capture());
+ final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = restartCaptor.getValue();
+ Assert.assertEquals(mTaskInfo, result.first);
+ Assert.assertEquals(mTaskListener, result.second);
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index e79b803..0c5edc3f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -28,6 +28,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -38,6 +39,7 @@
import android.app.TaskInfo;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
+import android.util.Pair;
import android.view.DisplayInfo;
import android.view.InsetsSource;
import android.view.InsetsState;
@@ -53,12 +55,17 @@
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
+import junit.framework.Assert;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.function.Consumer;
+
/**
* Tests for {@link CompatUIWindowManager}.
*
@@ -73,20 +80,22 @@
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private CompatUIController.CompatUICallback mCallback;
+ @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked;
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock private CompatUILayout mLayout;
@Mock private SurfaceControlViewHost mViewHost;
+ @Mock private CompatUIConfiguration mCompatUIConfiguration;
private CompatUIWindowManager mWindowManager;
+ private TaskInfo mTaskInfo;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
-
- mWindowManager = new CompatUIWindowManager(mContext,
- createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN),
- mSyncTransactionQueue, mCallback, mTaskListener,
- new DisplayLayout(), new CompatUIHintsState());
+ mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+ mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
+ mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+ mCompatUIConfiguration, mOnRestartButtonClicked);
spyOn(mWindowManager);
doReturn(mLayout).when(mWindowManager).inflateLayout();
@@ -351,14 +360,14 @@
mWindowManager.updateVisibility(/* canShow= */ false);
verify(mWindowManager, never()).createLayout(anyBoolean());
- verify(mLayout).setVisibility(View.GONE);
+ verify(mLayout, atLeastOnce()).setVisibility(View.GONE);
// Show button.
doReturn(View.GONE).when(mLayout).getVisibility();
mWindowManager.updateVisibility(/* canShow= */ true);
verify(mWindowManager, never()).createLayout(anyBoolean());
- verify(mLayout).setVisibility(View.VISIBLE);
+ verify(mLayout, atLeastOnce()).setVisibility(View.VISIBLE);
}
@Test
@@ -404,7 +413,14 @@
public void testOnRestartButtonClicked() {
mWindowManager.onRestartButtonClicked();
- verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID);
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> restartCaptor =
+ ArgumentCaptor.forClass(Pair.class);
+
+ verify(mOnRestartButtonClicked).accept(restartCaptor.capture());
+ final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result = restartCaptor.getValue();
+ Assert.assertEquals(mTaskInfo, result.first);
+ Assert.assertEquals(mTaskListener, result.second);
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
new file mode 100644
index 0000000..e2dcdb0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link RestartDialogLayout}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:RestartDialogLayoutTest
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class RestartDialogLayoutTest extends ShellTestCase {
+
+ @Mock private Runnable mDismissCallback;
+ @Mock private Consumer<Boolean> mRestartCallback;
+
+ private RestartDialogLayout mLayout;
+ private View mDismissButton;
+ private View mRestartButton;
+ private View mDialogContainer;
+ private CheckBox mDontRepeatCheckBox;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mLayout = (RestartDialogLayout)
+ LayoutInflater.from(mContext).inflate(R.layout.letterbox_restart_dialog_layout,
+ null);
+ mDismissButton = mLayout.findViewById(R.id.letterbox_restart_dialog_dismiss_button);
+ mRestartButton = mLayout.findViewById(R.id.letterbox_restart_dialog_restart_button);
+ mDialogContainer = mLayout.findViewById(R.id.letterbox_restart_dialog_container);
+ mDontRepeatCheckBox = mLayout.findViewById(R.id.letterbox_restart_dialog_checkbox);
+ mLayout.setDismissOnClickListener(mDismissCallback);
+ mLayout.setRestartOnClickListener(mRestartCallback);
+ }
+
+ @Test
+ public void testOnFinishInflate() {
+ assertEquals(mLayout.getDialogContainerView(),
+ mLayout.findViewById(R.id.letterbox_restart_dialog_container));
+ assertEquals(mLayout.getDialogTitle(),
+ mLayout.findViewById(R.id.letterbox_restart_dialog_title));
+ assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground());
+ assertEquals(mLayout.getBackground().getAlpha(), 0);
+ }
+
+ @Test
+ public void testOnDismissButtonClicked() {
+ assertTrue(mDismissButton.performClick());
+
+ verify(mDismissCallback).run();
+ }
+
+ @Test
+ public void testOnRestartButtonClickedWithoutCheckbox() {
+ mDontRepeatCheckBox.setChecked(false);
+ assertTrue(mRestartButton.performClick());
+
+ verify(mRestartCallback).accept(false);
+ }
+
+ @Test
+ public void testOnRestartButtonClickedWithCheckbox() {
+ mDontRepeatCheckBox.setChecked(true);
+ assertTrue(mRestartButton.performClick());
+
+ verify(mRestartCallback).accept(true);
+ }
+
+ @Test
+ public void testOnBackgroundClickedDoesntDismiss() {
+ assertFalse(mLayout.performClick());
+
+ verify(mDismissCallback, never()).run();
+ }
+
+ @Test
+ public void testOnDialogContainerClicked() {
+ assertTrue(mDialogContainer.performClick());
+
+ verify(mDismissCallback, never()).run();
+ verify(mRestartCallback, never()).accept(anyBoolean());
+ }
+
+ @Test
+ public void testSetDismissOnClickListenerNull() {
+ mLayout.setDismissOnClickListener(null);
+
+ assertFalse(mDismissButton.performClick());
+ assertFalse(mLayout.performClick());
+ assertTrue(mDialogContainer.performClick());
+
+ verify(mDismissCallback, never()).run();
+ }
+
+ @Test
+ public void testSetRestartOnClickListenerNull() {
+ mLayout.setRestartOnClickListener(null);
+
+ assertFalse(mRestartButton.performClick());
+ assertFalse(mLayout.performClick());
+ assertTrue(mDialogContainer.performClick());
+
+ verify(mRestartCallback, never()).accept(anyBoolean());
+ }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index 08af3d3..7997a7e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -279,7 +279,7 @@
}
@Test
- public void testShowDesktopApps_appsAlreadyVisible_doesNothing() {
+ public void testShowDesktopApps_appsAlreadyVisible_bringsToFront() {
final RunningTaskInfo task1 = createFreeformTask();
mDesktopModeTaskRepository.addActiveTask(task1.taskId);
mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
@@ -294,8 +294,17 @@
mController.showDesktopApps();
final WindowContainerTransaction wct = getBringAppsToFrontTransaction();
- // No reordering needed.
- assertThat(wct.getHierarchyOps()).isEmpty();
+ // Check wct has reorder calls
+ assertThat(wct.getHierarchyOps()).hasSize(2);
+ // Task 1 appeared first, must be first reorder to top.
+ HierarchyOp op1 = wct.getHierarchyOps().get(0);
+ assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op1.getContainer()).isEqualTo(task1.token.asBinder());
+
+ // Task 2 appeared last, must be last reorder to top.
+ HierarchyOp op2 = wct.getHierarchyOps().get(1);
+ assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+ assertThat(op2.getContainer()).isEqualTo(task2.token.asBinder());
}
@Test
@@ -325,6 +334,41 @@
}
@Test
+ public void testGetVisibleTaskCount_noTasks_returnsZero() {
+ assertThat(mController.getVisibleTaskCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void testGetVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+ RunningTaskInfo task1 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+
+ RunningTaskInfo task2 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, true /* visible */);
+
+ assertThat(mController.getVisibleTaskCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void testGetVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
+ RunningTaskInfo task1 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task1.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task1.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task1.taskId, true /* visible */);
+
+ RunningTaskInfo task2 = createFreeformTask();
+ mDesktopModeTaskRepository.addActiveTask(task2.taskId);
+ mDesktopModeTaskRepository.addOrMoveFreeformTaskToTop(task2.taskId);
+ mDesktopModeTaskRepository.updateVisibleFreeformTasks(task2.taskId, false /* visible */);
+
+ assertThat(mController.getVisibleTaskCount()).isEqualTo(1);
+ }
+
+ @Test
public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() {
when(DesktopModeStatus.isActive(any())).thenReturn(false);
WindowContainerTransaction wct = mController.handleRequest(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 1e43a59..45cb3a0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -141,6 +141,36 @@
}
@Test
+ fun getVisibleTaskCount() {
+ // No tasks, count is 0
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+
+ // New task increments count to 1
+ repo.updateVisibleFreeformTasks(taskId = 1, visible = true)
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+
+ // Visibility update to same task does not increase count
+ repo.updateVisibleFreeformTasks(taskId = 1, visible = true)
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+
+ // Second task visible increments count
+ repo.updateVisibleFreeformTasks(taskId = 2, visible = true)
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(2)
+
+ // Hiding a task decrements count
+ repo.updateVisibleFreeformTasks(taskId = 1, visible = false)
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(1)
+
+ // Hiding all tasks leaves count at 0
+ repo.updateVisibleFreeformTasks(taskId = 2, visible = false)
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+
+ // Hiding a not existing task, count remains at 0
+ repo.updateVisibleFreeformTasks(taskId = 999, visible = false)
+ assertThat(repo.getVisibleTaskCount()).isEqualTo(0)
+ }
+
+ @Test
fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() {
repo.addOrMoveFreeformTaskToTop(5)
repo.addOrMoveFreeformTaskToTop(6)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 9a92879..f16beee 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -150,8 +150,8 @@
}
@Test
- fun showDesktopApps_appsAlreadyVisible_doesNothing() {
- setUpHomeTask()
+ fun showDesktopApps_appsAlreadyVisible_bringsToFront() {
+ val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskVisible(task1)
@@ -159,7 +159,12 @@
controller.showDesktopApps()
- verifyWCTNotExecuted()
+ val wct = getLatestWct()
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: home, task1, task2
+ wct.assertReorderAt(index = 0, homeTask)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
}
@Test
@@ -192,6 +197,27 @@
}
@Test
+ fun getVisibleTaskCount_noTasks_returnsZero() {
+ assertThat(controller.getVisibleTaskCount()).isEqualTo(0)
+ }
+
+ @Test
+ fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+ setUpHomeTask()
+ setUpFreeformTask().also(::markTaskVisible)
+ setUpFreeformTask().also(::markTaskVisible)
+ assertThat(controller.getVisibleTaskCount()).isEqualTo(2)
+ }
+
+ @Test
+ fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
+ setUpHomeTask()
+ setUpFreeformTask().also(::markTaskVisible)
+ setUpFreeformTask().also(::markTaskHidden)
+ assertThat(controller.getVisibleTaskCount()).isEqualTo(1)
+ }
+
+ @Test
fun moveToDesktop() {
val task = setUpFullscreenTask()
controller.moveToDesktop(task)
@@ -207,6 +233,23 @@
}
@Test
+ fun moveToDesktop_otherFreeformTasksBroughtToFront() {
+ val homeTask = setUpHomeTask()
+ val freeformTask = setUpFreeformTask()
+ val fullscreenTask = setUpFullscreenTask()
+ markTaskHidden(freeformTask)
+
+ controller.moveToDesktop(fullscreenTask)
+
+ with(getLatestWct()) {
+ assertThat(hierarchyOps).hasSize(3)
+ assertReorderSequence(homeTask, freeformTask, fullscreenTask)
+ assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+ }
+
+ @Test
fun moveToFullscreen() {
val task = setUpFreeformTask()
controller.moveToFullscreen(task)
@@ -406,3 +449,9 @@
assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
assertThat(op.container).isEqualTo(task.token.asBinder())
}
+
+private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
+ for (i in tasks.indices) {
+ assertReorderAt(i, tasks[i])
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
index 262e429..298d0a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java
@@ -64,7 +64,7 @@
initializeMockResources();
mPipBoundsState = new PipBoundsState(mContext);
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
mPipBoundsState.setDisplayLayout(
new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 9088077..17e7d74 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -98,7 +98,7 @@
mPipBoundsState = new PipBoundsState(mContext);
mPipTransitionState = new PipTransitionState();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
- new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
+ new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {});
mMainExecutor = new TestShellExecutor();
mPipTaskOrganizer = new PipTaskOrganizer(mContext,
mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 3bd2ae7..c1993b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -37,7 +37,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
@@ -90,8 +90,8 @@
MockitoAnnotations.initMocks(this);
mPipBoundsState = new PipBoundsState(mContext);
final PipSnapAlgorithm pipSnapAlgorithm = new PipSnapAlgorithm();
- final PipKeepClearAlgorithm pipKeepClearAlgorithm =
- new PipKeepClearAlgorithm() {};
+ final PipKeepClearAlgorithmInterface pipKeepClearAlgorithm =
+ new PipKeepClearAlgorithmInterface() {};
final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm);
final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 474d6aa..8ad2932 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -34,7 +34,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.pip.PipBoundsAlgorithm;
import com.android.wm.shell.pip.PipBoundsState;
-import com.android.wm.shell.pip.PipKeepClearAlgorithm;
+import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface;
import com.android.wm.shell.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
@@ -106,7 +106,7 @@
mPipBoundsState = new PipBoundsState(mContext);
mPipSnapAlgorithm = new PipSnapAlgorithm();
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
- new PipKeepClearAlgorithm() {});
+ new PipKeepClearAlgorithmInterface() {});
PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
mMockPipTransitionController, mFloatingContentCoordinator);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 65e1ea8..0bb809d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -29,11 +29,13 @@
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -45,6 +47,8 @@
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
@@ -57,6 +61,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -65,6 +70,8 @@
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -98,11 +105,7 @@
@Mock
private DisplayInsetsController mDisplayInsetsController;
@Mock
- private Transitions mTransitions;
- @Mock
private TransactionPool mTransactionPool;
- @Mock
- private ShellExecutor mMainExecutor;
private final Rect mBounds1 = new Rect(10, 20, 30, 40);
private final Rect mBounds2 = new Rect(5, 10, 15, 20);
@@ -112,11 +115,16 @@
private SurfaceControl mRootLeash;
private ActivityManager.RunningTaskInfo mRootTask;
private StageCoordinator mStageCoordinator;
+ private Transitions mTransitions;
+ private final TestShellExecutor mMainExecutor = new TestShellExecutor();
+ private final ShellExecutor mAnimExecutor = new TestShellExecutor();
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
@Before
@UiThreadTest
public void setup() {
MockitoAnnotations.initMocks(this);
+ mTransitions = createTestTransitions();
mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
@@ -329,7 +337,20 @@
mStageCoordinator.onFoldedStateChanged(true);
- verify(mStageCoordinator).onSplitScreenExit();
- verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ verify(mTaskOrganizer).startNewTransition(eq(TRANSIT_SPLIT_DISMISS), notNull());
+ } else {
+ verify(mStageCoordinator).onSplitScreenExit();
+ verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
+ }
+ }
+
+ private Transitions createTestTransitions() {
+ ShellInit shellInit = new ShellInit(mMainExecutor);
+ final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
+ mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor,
+ mMainHandler, mAnimExecutor);
+ shellInit.init();
+ return t;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
new file mode 100644
index 0000000..8f84008
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.wm.shell.windowdecor
+
+import android.os.SystemClock
+import android.testing.AndroidTestingRunner
+import android.view.MotionEvent
+import android.view.InputDevice
+import androidx.test.filters.SmallTest
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * Tests for [DragDetector].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DragDetectorTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DragDetectorTest {
+ private val motionEvents = mutableListOf<MotionEvent>()
+
+ @Mock
+ private lateinit var eventHandler: DragDetector.MotionEventHandler
+
+ private lateinit var dragDetector: DragDetector
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ `when`(eventHandler.handleMotionEvent(any())).thenReturn(true)
+
+ dragDetector = DragDetector(eventHandler)
+ dragDetector.setTouchSlop(SLOP)
+ }
+
+ @After
+ fun tearDown() {
+ motionEvents.forEach {
+ it.recycle()
+ }
+ motionEvents.clear()
+ }
+
+ @Test
+ fun testNoMove_passesDownAndUp() {
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+ }
+
+ @Test
+ fun testMoveInSlop_touch_passesDownAndUp() {
+ `when`(eventHandler.handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN
+ })).thenReturn(false)
+
+ assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+
+ val newX = X + SLOP - 1
+ assertFalse(
+ dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
+ verify(eventHandler, never()).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE
+ })
+
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+ }
+
+ @Test
+ fun testMoveInSlop_mouse_passesDownMoveAndUp() {
+ `when`(eventHandler.handleMotionEvent(argThat {
+ it.action == MotionEvent.ACTION_DOWN
+ })).thenReturn(false)
+
+ assertFalse(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+
+ val newX = X + SLOP - 1
+ assertTrue(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+
+ assertTrue(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_UP, newX, Y, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+ }
+
+ @Test
+ fun testMoveBeyondSlop_passesDownMoveAndUp() {
+ `when`(eventHandler.handleMotionEvent(argThat {
+ it.action == MotionEvent.ACTION_DOWN
+ })).thenReturn(false)
+
+ assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+
+ val newX = X + SLOP + 1
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+ }
+
+ @Test
+ fun testPassesHoverEnter() {
+ `when`(eventHandler.handleMotionEvent(argThat {
+ it.action == MotionEvent.ACTION_HOVER_ENTER
+ })).thenReturn(false)
+
+ assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_ENTER)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_HOVER_ENTER && it.x == X && it.y == Y
+ })
+ }
+
+ @Test
+ fun testPassesHoverMove() {
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y
+ })
+ }
+
+ @Test
+ fun testPassesHoverExit() {
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT)))
+ verify(eventHandler).handleMotionEvent(argThat {
+ return@argThat it.action == MotionEvent.ACTION_HOVER_EXIT && it.x == X && it.y == Y
+ })
+ }
+
+ private fun createMotionEvent(action: Int, x: Float = X, y: Float = Y, isTouch: Boolean = true):
+ MotionEvent {
+ val time = SystemClock.uptimeMillis()
+ val ev = MotionEvent.obtain(time, time, action, x, y, 0)
+ ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE
+ motionEvents.add(ev)
+ return ev
+ }
+
+ companion object {
+ private const val SLOP = 10
+ private const val X = 123f
+ private const val Y = 234f
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
index ac10ddb..f185a8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
@@ -1,6 +1,7 @@
package com.android.wm.shell.windowdecor
import android.app.ActivityManager
+import android.app.WindowConfiguration
import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
@@ -10,6 +11,7 @@
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP
import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED
import org.junit.Before
import org.junit.Test
@@ -63,8 +65,92 @@
}
@Test
+ fun testDragResize_notMove_skipsTransactionOnEnd() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat() + 10
+ )
+
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+ }
+ })
+ }
+
+ @Test
+ fun testDragResize_noEffectiveMove_skipsTransactionOnMoveAndEnd() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat() + 10
+ )
+
+ verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
+ }
+ })
+ }
+
+ @Test
+ fun testDragResize_hasEffectiveMove_issuesTransactionOnMoveAndEnd() {
+ taskPositioner.onDragPositioningStart(
+ CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
+ STARTING_BOUNDS.left.toFloat(),
+ STARTING_BOUNDS.top.toFloat()
+ )
+
+ taskPositioner.onDragPositioningMove(
+ STARTING_BOUNDS.left.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat()
+ )
+ val rectAfterMove = Rect(STARTING_BOUNDS)
+ rectAfterMove.right += 10
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterMove
+ }
+ })
+
+ taskPositioner.onDragPositioningEnd(
+ STARTING_BOUNDS.left.toFloat() + 10,
+ STARTING_BOUNDS.top.toFloat() + 10
+ )
+ val rectAfterEnd = Rect(rectAfterMove)
+ rectAfterEnd.top += 10
+ verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ token == taskBinder &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
+ change.configuration.windowConfiguration.bounds == rectAfterEnd
+ }
+ })
+ }
+
+ @Test
fun testDragResize_move_skipsDragResizingFlag() {
- taskPositioner.onDragResizeStart(
+ taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // Move
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
@@ -73,12 +159,12 @@
// Move the task 10px to the right.
val newX = STARTING_BOUNDS.left.toFloat() + 10
val newY = STARTING_BOUNDS.top.toFloat()
- taskPositioner.onDragResizeMove(
+ taskPositioner.onDragPositioningMove(
newX,
newY
)
- taskPositioner.onDragResizeEnd(newX, newY)
+ taskPositioner.onDragPositioningEnd(newX, newY)
verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
@@ -91,7 +177,7 @@
@Test
fun testDragResize_resize_setsDragResizingFlag() {
- taskPositioner.onDragResizeStart(
+ taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
@@ -100,12 +186,12 @@
// Resize the task by 10px to the right.
val newX = STARTING_BOUNDS.right.toFloat() + 10
val newY = STARTING_BOUNDS.top.toFloat()
- taskPositioner.onDragResizeMove(
+ taskPositioner.onDragPositioningMove(
newX,
newY
)
- taskPositioner.onDragResizeEnd(newX, newY)
+ taskPositioner.onDragPositioningEnd(newX, newY)
verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index ec4f17f..2f5263c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -107,6 +107,7 @@
private SurfaceControl.Transaction mMockSurfaceControlFinishT;
private SurfaceControl.Transaction mMockSurfaceControlAddWindowT;
private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
+ private int mCaptionMenuWidthId;
@Before
public void setUp() {
@@ -116,8 +117,7 @@
mRelayoutParams.mLayoutResId = 0;
mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
- // Caption should have fixed width except in testLayoutResultCalculation_fullWidthCaption()
- mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width;
+ mCaptionMenuWidthId = R.dimen.test_freeform_decor_caption_menu_width;
mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
@@ -240,7 +240,7 @@
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
- verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 432, 64);
+ verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
verify(mMockSurfaceControlStartT).show(captionContainerSurface);
verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
@@ -248,7 +248,7 @@
verify(mMockSurfaceControlViewHost)
.setView(same(mMockView),
argThat(lp -> lp.height == 64
- && lp.width == 432
+ && lp.width == 300
&& (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
if (ViewRootImpl.CAPTION_ON_SHELL) {
verify(mMockView).setTaskFocusState(true);
@@ -484,7 +484,6 @@
final SurfaceControl taskSurface = mock(SurfaceControl.class);
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
- mRelayoutParams.mCaptionWidthId = Resources.ID_NULL;
windowDecor.relayout(taskInfo);
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
@@ -558,7 +557,7 @@
final Resources resources = mDecorWindowContext.getResources();
int x = mRelayoutParams.mCaptionX;
int y = mRelayoutParams.mCaptionY;
- int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+ int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId);
int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
String name = "Test Window";
WindowDecoration.AdditionalWindow additionalWindow =
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3aa2338..f1768b9 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1337,12 +1337,8 @@
public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags) {
Preconditions.checkNotNull(attr, "attr must not be null");
final IAudioService service = getService();
- try {
- service.setVolumeIndexForAttributes(attr, index, flags,
- getContext().getOpPackageName(), getContext().getAttributionTag());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ int groupId = getVolumeGroupIdForAttributes(attr);
+ setVolumeGroupVolumeIndex(groupId, index, flags);
}
/**
@@ -1361,11 +1357,8 @@
public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
Preconditions.checkNotNull(attr, "attr must not be null");
final IAudioService service = getService();
- try {
- return service.getVolumeIndexForAttributes(attr);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ int groupId = getVolumeGroupIdForAttributes(attr);
+ return getVolumeGroupVolumeIndex(groupId);
}
/**
@@ -1382,11 +1375,8 @@
public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
Preconditions.checkNotNull(attr, "attr must not be null");
final IAudioService service = getService();
- try {
- return service.getMaxVolumeIndexForAttributes(attr);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ int groupId = getVolumeGroupIdForAttributes(attr);
+ return getVolumeGroupMaxVolumeIndex(groupId);
}
/**
@@ -1403,8 +1393,168 @@
public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
Preconditions.checkNotNull(attr, "attr must not be null");
final IAudioService service = getService();
+ int groupId = getVolumeGroupIdForAttributes(attr);
+ return getVolumeGroupMinVolumeIndex(groupId);
+ }
+
+ /**
+ * Returns the volume group id associated to the given {@link AudioAttributes}.
+ *
+ * @param attributes The {@link AudioAttributes} to consider.
+ * @return {@link android.media.audiopolicy.AudioVolumeGroup} id supporting the given
+ * {@link AudioAttributes} if found,
+ * {@code android.media.audiopolicy.AudioVolumeGroup.DEFAULT_VOLUME_GROUP} otherwise.
+ * @hide
+ */
+ public int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) {
+ Preconditions.checkNotNull(attributes, "Audio Attributes must not be null");
+ return AudioProductStrategy.getVolumeGroupIdForAudioAttributes(attributes,
+ /* fallbackOnDefault= */ false);
+ }
+
+ /**
+ * Sets the volume index for a particular group associated to given id.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @param index The volume index to set. See
+ * {@link #getVolumeGroupMaxVolumeIndex(id)} for the largest valid value
+ * {@link #getVolumeGroupMinVolumeIndex(id)} for the lowest valid value.
+ * @param flags One or more flags.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void setVolumeGroupVolumeIndex(int groupId, int index, int flags) {
+ final IAudioService service = getService();
try {
- return service.getMinVolumeIndexForAttributes(attr);
+ service.setVolumeGroupVolumeIndex(groupId, index, flags,
+ getContext().getOpPackageName(), getContext().getAttributionTag());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current volume index for a particular group associated to given id.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @return The current volume index for the stream.
+ * @hide
+ */
+ @IntRange(from = 0)
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int getVolumeGroupVolumeIndex(int groupId) {
+ final IAudioService service = getService();
+ try {
+ return service.getVolumeGroupVolumeIndex(groupId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the maximum volume index for a particular group associated to given id.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @return The maximum valid volume index for the {@link AudioAttributes}.
+ * @hide
+ */
+ @IntRange(from = 0)
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int getVolumeGroupMaxVolumeIndex(int groupId) {
+ final IAudioService service = getService();
+ try {
+ return service.getVolumeGroupMaxVolumeIndex(groupId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the minimum volume index for a particular group associated to given id.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @return The minimum valid volume index for the {@link AudioAttributes}.
+ * @hide
+ */
+ @IntRange(from = 0)
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int getVolumeGroupMinVolumeIndex(int groupId) {
+ final IAudioService service = getService();
+ try {
+ return service.getVolumeGroupMinVolumeIndex(groupId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adjusts the volume of a particular group associated to given id by one step in a direction.
+ * <p> If the volume group is associated to a stream type, it fallbacks on
+ * {@link AudioManager#adjustStreamVolume()} for compatibility reason.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @param direction The direction to adjust the volume. One of
+ * {@link #ADJUST_LOWER}, {@link #ADJUST_RAISE}, or
+ * {@link #ADJUST_SAME}.
+ * @param flags One or more flags.
+ * @throws SecurityException if the adjustment triggers a Do Not Disturb change and the caller
+ * is not granted notification policy access.
+ * @hide
+ */
+ public void adjustVolumeGroupVolume(int groupId, int direction, int flags) {
+ IAudioService service = getService();
+ try {
+ service.adjustVolumeGroupVolume(groupId, direction, flags,
+ getContext().getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get last audible volume of the group associated to given id before it was muted.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @return current volume if not muted, volume before muted otherwise.
+ * @hide
+ */
+ @RequiresPermission("android.permission.QUERY_AUDIO_STATE")
+ @IntRange(from = 0)
+ public int getLastAudibleVolumeGroupVolume(int groupId) {
+ IAudioService service = getService();
+ try {
+ return service.getLastAudibleVolumeGroupVolume(groupId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current mute state for a particular volume group associated to the given id.
+ * <p> Call first in prior {@link getVolumeGroupIdForAttributes} to retrieve the volume group
+ * id supporting the given {@link AudioAttributes}.
+ *
+ * @param groupId of the {@link android.media.audiopolicy.AudioVolumeGroup} to consider.
+ * @return The mute state for the given {@link android.media.audiopolicy.AudioVolumeGroup} id.
+ * @see #adjustAttributesVolume(AudioAttributes, int, int)
+ * @hide
+ */
+ public boolean isVolumeGroupMuted(int groupId) {
+ IAudioService service = getService();
+ try {
+ return service.isVolumeGroupMuted(groupId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -7688,7 +7838,8 @@
Objects.requireNonNull(device);
try {
if (device.getId() == 0) {
- throw new IllegalArgumentException("In valid device: " + device);
+ Log.w(TAG, "setCommunicationDevice: device not found: " + device);
+ return false;
}
return getService().setCommunicationDevice(mICallBack, device.getId());
} catch (RemoteException e) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index aa2e0ba..73dbfa8 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -127,14 +127,20 @@
List<AudioVolumeGroup> getAudioVolumeGroups();
- void setVolumeIndexForAttributes(in AudioAttributes aa, int index, int flags,
- String callingPackage, in String attributionTag);
+ void setVolumeGroupVolumeIndex(int groupId, int index, int flags, String callingPackage,
+ in String attributionTag);
- int getVolumeIndexForAttributes(in AudioAttributes aa);
+ int getVolumeGroupVolumeIndex(int groupId);
- int getMaxVolumeIndexForAttributes(in AudioAttributes aa);
+ int getVolumeGroupMaxVolumeIndex(int groupId);
- int getMinVolumeIndexForAttributes(in AudioAttributes aa);
+ int getVolumeGroupMinVolumeIndex(int groupId);
+
+ int getLastAudibleVolumeGroupVolume(int groupId);
+
+ boolean isVolumeGroupMuted(int groupId);
+
+ void adjustVolumeGroupVolume(int groupId, int direction, int flags, String callingPackage);
int getLastAudibleStreamVolume(int streamType);
diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java
index f957498..4b46d1a 100644
--- a/media/java/android/media/audiopolicy/AudioProductStrategy.java
+++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java
@@ -29,11 +29,11 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
/**
* @hide
@@ -143,7 +143,7 @@
*/
public static int getLegacyStreamTypeForStrategyWithAudioAttributes(
@NonNull AudioAttributes audioAttributes) {
- Preconditions.checkNotNull(audioAttributes, "AudioAttributes must not be null");
+ Objects.requireNonNull(audioAttributes, "AudioAttributes must not be null");
for (final AudioProductStrategy productStrategy :
AudioProductStrategy.getAudioProductStrategies()) {
if (productStrategy.supportsAudioAttributes(audioAttributes)) {
@@ -163,6 +163,30 @@
return AudioSystem.STREAM_MUSIC;
}
+ /**
+ * @hide
+ * @param attributes the {@link AudioAttributes} to identify VolumeGroupId with
+ * @param fallbackOnDefault if set, allows to fallback on the default group (e.g. the group
+ * associated to {@link AudioManager#STREAM_MUSIC}).
+ * @return volume group id associated with the given {@link AudioAttributes} if found,
+ * default volume group id if fallbackOnDefault is set
+ * <p>By convention, the product strategy with default attributes will be associated to the
+ * default volume group (e.g. associated to {@link AudioManager#STREAM_MUSIC})
+ * or {@link AudioVolumeGroup#DEFAULT_VOLUME_GROUP} if not found.
+ */
+ public static int getVolumeGroupIdForAudioAttributes(
+ @NonNull AudioAttributes attributes, boolean fallbackOnDefault) {
+ Objects.requireNonNull(attributes, "attributes must not be null");
+ int volumeGroupId = getVolumeGroupIdForAudioAttributesInt(attributes);
+ if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
+ return volumeGroupId;
+ }
+ if (fallbackOnDefault) {
+ return getVolumeGroupIdForAudioAttributesInt(getDefaultAttributes());
+ }
+ return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+ }
+
private static List<AudioProductStrategy> initializeAudioProductStrategies() {
ArrayList<AudioProductStrategy> apsList = new ArrayList<AudioProductStrategy>();
int status = native_list_audio_product_strategies(apsList);
@@ -193,8 +217,8 @@
*/
private AudioProductStrategy(@NonNull String name, int id,
@NonNull AudioAttributesGroup[] aag) {
- Preconditions.checkNotNull(name, "name must not be null");
- Preconditions.checkNotNull(aag, "AudioAttributesGroups must not be null");
+ Objects.requireNonNull(name, "name must not be null");
+ Objects.requireNonNull(aag, "AudioAttributesGroups must not be null");
mName = name;
mId = id;
mAudioAttributesGroups = aag;
@@ -212,6 +236,15 @@
/**
* @hide
+ * @return the product strategy ID (which is the generalisation of Car Audio Usage / legacy
+ * routing_strategy linked to {@link AudioAttributes#getUsage()}).
+ */
+ @NonNull public String getName() {
+ return mName;
+ }
+
+ /**
+ * @hide
* @return first {@link AudioAttributes} associated to this product strategy.
*/
@SystemApi
@@ -244,7 +277,7 @@
*/
@TestApi
public int getLegacyStreamTypeForAudioAttributes(@NonNull AudioAttributes aa) {
- Preconditions.checkNotNull(aa, "AudioAttributes must not be null");
+ Objects.requireNonNull(aa, "AudioAttributes must not be null");
for (final AudioAttributesGroup aag : mAudioAttributesGroups) {
if (aag.supportsAttributes(aa)) {
return aag.getStreamType();
@@ -261,7 +294,7 @@
*/
@SystemApi
public boolean supportsAudioAttributes(@NonNull AudioAttributes aa) {
- Preconditions.checkNotNull(aa, "AudioAttributes must not be null");
+ Objects.requireNonNull(aa, "AudioAttributes must not be null");
for (final AudioAttributesGroup aag : mAudioAttributesGroups) {
if (aag.supportsAttributes(aa)) {
return true;
@@ -294,7 +327,7 @@
*/
@TestApi
public int getVolumeGroupIdForAudioAttributes(@NonNull AudioAttributes aa) {
- Preconditions.checkNotNull(aa, "AudioAttributes must not be null");
+ Objects.requireNonNull(aa, "AudioAttributes must not be null");
for (final AudioAttributesGroup aag : mAudioAttributesGroups) {
if (aag.supportsAttributes(aa)) {
return aag.getVolumeGroupId();
@@ -303,6 +336,17 @@
return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
}
+ private static int getVolumeGroupIdForAudioAttributesInt(@NonNull AudioAttributes attributes) {
+ Objects.requireNonNull(attributes, "attributes must not be null");
+ for (AudioProductStrategy productStrategy : getAudioProductStrategies()) {
+ int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes);
+ if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
+ return volumeGroupId;
+ }
+ }
+ return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -378,8 +422,8 @@
*/
private static boolean attributesMatches(@NonNull AudioAttributes refAttr,
@NonNull AudioAttributes attr) {
- Preconditions.checkNotNull(refAttr, "refAttr must not be null");
- Preconditions.checkNotNull(attr, "attr must not be null");
+ Objects.requireNonNull(refAttr, "reference AudioAttributes must not be null");
+ Objects.requireNonNull(attr, "requester's AudioAttributes must not be null");
String refFormattedTags = TextUtils.join(";", refAttr.getTags());
String cliFormattedTags = TextUtils.join(";", attr.getTags());
if (refAttr.equals(DEFAULT_ATTRIBUTES)) {
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 468a976..1592094 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -59,6 +59,18 @@
private Uri mImageUri;
private Drawable mImageDrawable;
private View mMiddleGroundView;
+ private OnBindListener mOnBindListener;
+
+ /**
+ * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+ */
+ public interface OnBindListener {
+ /**
+ * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
+ * @param animationView the animation view for this preference.
+ */
+ void onBind(LottieAnimationView animationView);
+ }
private final Animatable2.AnimationCallback mAnimationCallback =
new Animatable2.AnimationCallback() {
@@ -133,6 +145,17 @@
if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) {
ColorUtils.applyDynamicColors(getContext(), illustrationView);
}
+
+ if (mOnBindListener != null) {
+ mOnBindListener.onBind(illustrationView);
+ }
+ }
+
+ /**
+ * Sets a listener to be notified when the views are binded.
+ */
+ public void setOnBindListener(OnBindListener listener) {
+ mOnBindListener = listener;
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
index 44a37f4..d4d2b48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.content.res.Resources;
+import android.hardware.display.DisplayManager;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -32,6 +33,9 @@
import com.android.settingslib.R;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
/**
* Utility methods for working with display density.
@@ -70,120 +74,169 @@
*/
private static final int MIN_DIMENSION_DP = 320;
- private final String[] mEntries;
- private final int[] mValues;
+ private static final Predicate<DisplayInfo> INTERNAL_ONLY =
+ (info) -> info.type == Display.TYPE_INTERNAL;
- private final int mDefaultDensity;
- private final int mCurrentIndex;
+ private final Predicate<DisplayInfo> mPredicate;
+
+ private final DisplayManager mDisplayManager;
+
+ /**
+ * The text description of the density values of the default display.
+ */
+ private String[] mDefaultDisplayDensityEntries;
+
+ /**
+ * The density values of the default display.
+ */
+ private int[] mDefaultDisplayDensityValues;
+
+ /**
+ * The density values, indexed by display unique ID.
+ */
+ private final Map<String, int[]> mValuesPerDisplay = new HashMap();
+
+ private int mDefaultDensityForDefaultDisplay;
+ private int mCurrentIndex = -1;
public DisplayDensityUtils(Context context) {
- final int defaultDensity = DisplayDensityUtils.getDefaultDisplayDensity(
- Display.DEFAULT_DISPLAY);
- if (defaultDensity <= 0) {
- mEntries = null;
- mValues = null;
- mDefaultDensity = 0;
- mCurrentIndex = -1;
- return;
- }
+ this(context, INTERNAL_ONLY);
+ }
- final Resources res = context.getResources();
- DisplayInfo info = new DisplayInfo();
- context.getDisplayNoVerify().getDisplayInfo(info);
+ /**
+ * Creates an instance that stores the density values for the displays that satisfy
+ * the predicate.
+ * @param context The context
+ * @param predicate Determines what displays the density should be set for. The default display
+ * must satisfy this predicate.
+ */
+ public DisplayDensityUtils(Context context, Predicate predicate) {
+ mPredicate = predicate;
+ mDisplayManager = context.getSystemService(DisplayManager.class);
- final int currentDensity = info.logicalDensityDpi;
- int currentDensityIndex = -1;
-
- // Compute number of "larger" and "smaller" scales for this display.
- final int minDimensionPx = Math.min(info.logicalWidth, info.logicalHeight);
- final int maxDensity = DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
- final float maxScaleDimen = context.getResources().getFraction(
- R.fraction.display_density_max_scale, 1, 1);
- final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity);
- final float minScale = context.getResources().getFraction(
- R.fraction.display_density_min_scale, 1, 1);
- final float minScaleInterval = context.getResources().getFraction(
- R.fraction.display_density_min_scale_interval, 1, 1);
- final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval,
- 0, SUMMARIES_LARGER.length);
- final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval,
- 0, SUMMARIES_SMALLER.length);
-
- String[] entries = new String[1 + numSmaller + numLarger];
- int[] values = new int[entries.length];
- int curIndex = 0;
-
- if (numSmaller > 0) {
- final float interval = (1 - minScale) / numSmaller;
- for (int i = numSmaller - 1; i >= 0; i--) {
- // Round down to a multiple of 2 by truncating the low bit.
- final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
- if (currentDensity == density) {
- currentDensityIndex = curIndex;
- }
- entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
- values[curIndex] = density;
- curIndex++;
+ for (Display display : mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+ DisplayInfo info = new DisplayInfo();
+ if (!display.getDisplayInfo(info)) {
+ Log.w(LOG_TAG, "Cannot fetch display info for display " + display.getDisplayId());
+ continue;
}
- }
-
- if (currentDensity == defaultDensity) {
- currentDensityIndex = curIndex;
- }
- values[curIndex] = defaultDensity;
- entries[curIndex] = res.getString(SUMMARY_DEFAULT);
- curIndex++;
-
- if (numLarger > 0) {
- final float interval = (maxScale - 1) / numLarger;
- for (int i = 0; i < numLarger; i++) {
- // Round down to a multiple of 2 by truncating the low bit.
- final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
- if (currentDensity == density) {
- currentDensityIndex = curIndex;
+ if (!mPredicate.test(info)) {
+ if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ throw new IllegalArgumentException("Predicate must not filter out the default "
+ + "display.");
}
- values[curIndex] = density;
- entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
- curIndex++;
+ continue;
}
+
+ final int defaultDensity = DisplayDensityUtils.getDefaultDensityForDisplay(
+ display.getDisplayId());
+ if (defaultDensity <= 0) {
+ Log.w(LOG_TAG, "Cannot fetch default density for display "
+ + display.getDisplayId());
+ continue;
+ }
+
+ final Resources res = context.getResources();
+
+ final int currentDensity = info.logicalDensityDpi;
+ int currentDensityIndex = -1;
+
+ // Compute number of "larger" and "smaller" scales for this display.
+ final int minDimensionPx = Math.min(info.logicalWidth, info.logicalHeight);
+ final int maxDensity =
+ DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
+ final float maxScaleDimen = context.getResources().getFraction(
+ R.fraction.display_density_max_scale, 1, 1);
+ final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity);
+ final float minScale = context.getResources().getFraction(
+ R.fraction.display_density_min_scale, 1, 1);
+ final float minScaleInterval = context.getResources().getFraction(
+ R.fraction.display_density_min_scale_interval, 1, 1);
+ final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval,
+ 0, SUMMARIES_LARGER.length);
+ final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval,
+ 0, SUMMARIES_SMALLER.length);
+
+ String[] entries = new String[1 + numSmaller + numLarger];
+ int[] values = new int[entries.length];
+ int curIndex = 0;
+
+ if (numSmaller > 0) {
+ final float interval = (1 - minScale) / numSmaller;
+ for (int i = numSmaller - 1; i >= 0; i--) {
+ // Round down to a multiple of 2 by truncating the low bit.
+ final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
+ if (currentDensity == density) {
+ currentDensityIndex = curIndex;
+ }
+ entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
+ values[curIndex] = density;
+ curIndex++;
+ }
+ }
+
+ if (currentDensity == defaultDensity) {
+ currentDensityIndex = curIndex;
+ }
+ values[curIndex] = defaultDensity;
+ entries[curIndex] = res.getString(SUMMARY_DEFAULT);
+ curIndex++;
+
+ if (numLarger > 0) {
+ final float interval = (maxScale - 1) / numLarger;
+ for (int i = 0; i < numLarger; i++) {
+ // Round down to a multiple of 2 by truncating the low bit.
+ final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
+ if (currentDensity == density) {
+ currentDensityIndex = curIndex;
+ }
+ values[curIndex] = density;
+ entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
+ curIndex++;
+ }
+ }
+
+ final int displayIndex;
+ if (currentDensityIndex >= 0) {
+ displayIndex = currentDensityIndex;
+ } else {
+ // We don't understand the current density. Must have been set by
+ // someone else. Make room for another entry...
+ int newLength = values.length + 1;
+ values = Arrays.copyOf(values, newLength);
+ values[curIndex] = currentDensity;
+
+ entries = Arrays.copyOf(entries, newLength);
+ entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
+
+ displayIndex = curIndex;
+ }
+
+ if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ mDefaultDensityForDefaultDisplay = defaultDensity;
+ mCurrentIndex = displayIndex;
+ mDefaultDisplayDensityEntries = entries;
+ mDefaultDisplayDensityValues = values;
+ }
+ mValuesPerDisplay.put(info.uniqueId, values);
}
-
- final int displayIndex;
- if (currentDensityIndex >= 0) {
- displayIndex = currentDensityIndex;
- } else {
- // We don't understand the current density. Must have been set by
- // someone else. Make room for another entry...
- int newLength = values.length + 1;
- values = Arrays.copyOf(values, newLength);
- values[curIndex] = currentDensity;
-
- entries = Arrays.copyOf(entries, newLength);
- entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
-
- displayIndex = curIndex;
- }
-
- mDefaultDensity = defaultDensity;
- mCurrentIndex = displayIndex;
- mEntries = entries;
- mValues = values;
}
- public String[] getEntries() {
- return mEntries;
+ public String[] getDefaultDisplayDensityEntries() {
+ return mDefaultDisplayDensityEntries;
}
- public int[] getValues() {
- return mValues;
+ public int[] getDefaultDisplayDensityValues() {
+ return mDefaultDisplayDensityValues;
}
- public int getCurrentIndex() {
+ public int getCurrentIndexForDefaultDisplay() {
return mCurrentIndex;
}
- public int getDefaultDensity() {
- return mDefaultDensity;
+ public int getDefaultDensityForDefaultDisplay() {
+ return mDefaultDensityForDefaultDisplay;
}
/**
@@ -193,7 +246,7 @@
* @return the default density of the specified display, or {@code -1} if
* the display does not exist or the density could not be obtained
*/
- private static int getDefaultDisplayDensity(int displayId) {
+ private static int getDefaultDensityForDisplay(int displayId) {
try {
final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
return wm.getInitialDisplayDensity(displayId);
@@ -203,19 +256,31 @@
}
/**
- * Asynchronously applies display density changes to the specified display.
+ * Asynchronously applies display density changes to the displays that satisfy the predicate.
* <p>
* The change will be applied to the user specified by the value of
* {@link UserHandle#myUserId()} at the time the method is called.
- *
- * @param displayId the identifier of the display to modify
*/
- public static void clearForcedDisplayDensity(final int displayId) {
+ public void clearForcedDisplayDensity() {
final int userId = UserHandle.myUserId();
AsyncTask.execute(() -> {
try {
- final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
- wm.clearForcedDisplayDensityForUser(displayId, userId);
+ for (Display display : mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+ int displayId = display.getDisplayId();
+ DisplayInfo info = new DisplayInfo();
+ if (!display.getDisplayInfo(info)) {
+ Log.w(LOG_TAG, "Unable to clear forced display density setting "
+ + "for display " + displayId);
+ continue;
+ }
+ if (!mPredicate.test(info)) {
+ continue;
+ }
+
+ final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ wm.clearForcedDisplayDensityForUser(displayId, userId);
+ }
} catch (RemoteException exc) {
Log.w(LOG_TAG, "Unable to clear forced display density setting");
}
@@ -223,20 +288,39 @@
}
/**
- * Asynchronously applies display density changes to the specified display.
+ * Asynchronously applies display density changes to the displays that satisfy the predicate.
* <p>
* The change will be applied to the user specified by the value of
* {@link UserHandle#myUserId()} at the time the method is called.
*
- * @param displayId the identifier of the display to modify
- * @param density the density to force for the specified display
+ * @param index The index of the density value
*/
- public static void setForcedDisplayDensity(final int displayId, final int density) {
+ public void setForcedDisplayDensity(final int index) {
final int userId = UserHandle.myUserId();
AsyncTask.execute(() -> {
try {
- final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
- wm.setForcedDisplayDensityForUser(displayId, density, userId);
+ for (Display display : mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+ int displayId = display.getDisplayId();
+ DisplayInfo info = new DisplayInfo();
+ if (!display.getDisplayInfo(info)) {
+ Log.w(LOG_TAG, "Unable to save forced display density setting "
+ + "for display " + displayId);
+ continue;
+ }
+ if (!mPredicate.test(info)) {
+ continue;
+ }
+ if (!mValuesPerDisplay.containsKey(info.uniqueId)) {
+ Log.w(LOG_TAG, "Unable to save forced display density setting "
+ + "for display " + info.uniqueId);
+ continue;
+ }
+
+ final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ wm.setForcedDisplayDensityForUser(displayId,
+ mValuesPerDisplay.get(info.uniqueId)[index], userId);
+ }
} catch (RemoteException exc) {
Log.w(LOG_TAG, "Unable to save forced display density setting");
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 7ec0fcd..d2e615a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -190,6 +190,10 @@
return !isGroup;
}
+ boolean preferRouteListingOrdering() {
+ return false;
+ }
+
/**
* Remove a {@code device} from current media.
*
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index f4355c3..7458f010 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -213,6 +213,15 @@
}
/**
+ * Returns if media app establishes a preferred route listing order.
+ *
+ * @return True if route list ordering exist and not using system ordering, false otherwise.
+ */
+ public boolean isPreferenceRouteListingExist() {
+ return mInfoMediaManager.preferRouteListingOrdering();
+ }
+
+ /**
* Start scan connected MediaDevice
*/
public void startScan() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index c829bc3..d9d7cc9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -184,6 +184,33 @@
*/
public abstract String getId();
+ /**
+ * Get disabled reason of device
+ *
+ * @return disabled reason of device
+ */
+ public int getDisableReason() {
+ return -1;
+ }
+
+ /**
+ * Checks if device is has disabled reason
+ *
+ * @return true if device has disabled reason
+ */
+ public boolean hasDisabledReason() {
+ return false;
+ }
+
+ /**
+ * Checks if device is suggested device from application
+ *
+ * @return true if device is suggested device
+ */
+ public boolean isSuggestedDevice() {
+ return false;
+ }
+
void setConnectedRecord() {
mConnectedRecord++;
ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(),
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
index 29549d9..103512d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java
@@ -61,6 +61,8 @@
private PreferenceViewHolder mViewHolder;
private FrameLayout mMiddleGroundLayout;
private final Context mContext = ApplicationProvider.getApplicationContext();
+ private IllustrationPreference.OnBindListener mOnBindListener;
+ private LottieAnimationView mOnBindListenerAnimationView;
@Before
public void setUp() {
@@ -82,6 +84,12 @@
final AttributeSet attributeSet = Robolectric.buildAttributeSet().build();
mPreference = new IllustrationPreference(mContext, attributeSet);
+ mOnBindListener = new IllustrationPreference.OnBindListener() {
+ @Override
+ public void onBind(LottieAnimationView animationView) {
+ mOnBindListenerAnimationView = animationView;
+ }
+ };
}
@Test
@@ -186,4 +194,25 @@
assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight);
assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight);
}
+
+ @Test
+ public void setOnBindListener_isNotified() {
+ mOnBindListenerAnimationView = null;
+ mPreference.setOnBindListener(mOnBindListener);
+
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mOnBindListenerAnimationView).isNotNull();
+ assertThat(mOnBindListenerAnimationView).isEqualTo(mAnimationView);
+ }
+
+ @Test
+ public void setOnBindListener_notNotified() {
+ mOnBindListenerAnimationView = null;
+ mPreference.setOnBindListener(null);
+
+ mPreference.onBindViewHolder(mViewHolder);
+
+ assertThat(mOnBindListenerAnimationView).isNull();
+ }
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
new file mode 100644
index 0000000..bdd4869
--- /dev/null
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/LargeScreenSettings.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider.settings.backup;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+/** Settings that should not be restored when target device is a large screen
+ * i.e. tablets and foldables in unfolded state
+ */
+public class LargeScreenSettings {
+ private static final float LARGE_SCREEN_MIN_DPS = 600;
+ private static final String LARGE_SCREEN_DO_NOT_RESTORE = "accelerometer_rotation";
+
+ /**
+ * Autorotation setting should not be restored when the target device is a large screen.
+ * (b/243489549)
+ */
+ public static boolean doNotRestoreIfLargeScreenSetting(String key, Context context) {
+ return isLargeScreen(context) && LARGE_SCREEN_DO_NOT_RESTORE.equals(key);
+ }
+
+ // copied from systemui/shared/...Utilities.java
+ // since we don't want to add compile time dependency on sys ui package
+ private static boolean isLargeScreen(Context context) {
+ final WindowManager windowManager = context.getSystemService(WindowManager.class);
+ final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
+ float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
+ context.getResources().getConfiguration().densityDpi);
+ return smallestWidth >= LARGE_SCREEN_MIN_DPS;
+ }
+
+ private static float dpiFromPx(float size, int densityDpi) {
+ float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
+ return (size / densityRatio);
+ }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 1b0b6b4..211030a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -123,7 +123,7 @@
Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW,
Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME,
- Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
+ Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED,
Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 4fa490f..0539f09 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -178,7 +178,7 @@
VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR);
- VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.SFPS_PERFORMANT_AUTH_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index d3afccc..d0055d7 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -37,6 +37,7 @@
import android.provider.Settings;
import android.provider.settings.backup.DeviceSpecificSettings;
import android.provider.settings.backup.GlobalSettings;
+import android.provider.settings.backup.LargeScreenSettings;
import android.provider.settings.backup.SecureSettings;
import android.provider.settings.backup.SystemSettings;
import android.provider.settings.validators.GlobalSettingsValidators;
@@ -812,6 +813,12 @@
continue;
}
+ if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
+ Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
+ + "is a large screen (i.e tablet or foldable in unfolded state)");
+ continue;
+ }
+
String value = null;
boolean hasValueToRestore = false;
if (cachedEntries.indexOfKey(key) >= 0) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cd0fbea..c67cdfc 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -271,6 +271,7 @@
"LowLightDreamLib",
"motion_tool_lib",
"androidx.core_core-animation-testing-nodeps",
+ "androidx.compose.ui_ui",
],
}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 810dd33..e96aead5 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -292,7 +292,7 @@
<queries>
<intent>
- <action android:name="android.intent.action.NOTES" />
+ <action android:name="android.intent.action.CREATE_NOTE" />
</intent>
</queries>
@@ -411,7 +411,6 @@
<service android:name=".screenshot.ScreenshotCrossProfileService"
android:permission="com.android.systemui.permission.SELF"
- android:process=":screenshot_cross_profile"
android:exported="false" />
<service android:name=".screenrecord.RecordingService" />
@@ -664,6 +663,21 @@
android:excludeFromRecents="true"
android:exported="true" />
+ <!-- started from Telecomm(CallsManager) -->
+ <activity
+ android:name=".telephony.ui.activity.SwitchToManagedProfileForCallActivity"
+ android:excludeFromRecents="true"
+ android:exported="true"
+ android:finishOnCloseSystemDialogs="true"
+ android:permission="android.permission.MODIFY_PHONE_STATE"
+ android:theme="@style/Theme.SystemUI.Dialog.Alert">
+ <intent-filter>
+ <action android:name="android.telecom.action.SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="tel" />
+ </intent-filter>
+ </activity>
+
<!-- platform logo easter egg activity -->
<activity
android:name=".DessertCase"
@@ -908,7 +922,7 @@
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/note_task_button_label"
- android:icon="@drawable/ic_note_task_button">
+ android:icon="@drawable/ic_note_task_shortcut_widget">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
diff --git a/packages/SystemUI/README.md b/packages/SystemUI/README.md
index ee8d023..2910bba 100644
--- a/packages/SystemUI/README.md
+++ b/packages/SystemUI/README.md
@@ -5,46 +5,72 @@
SystemUI is a persistent process that provides UI for the system but outside
of the system_server process.
-The starting point for most of sysui code is a list of services that extend
-SystemUI that are started up by SystemUIApplication. These services then depend
-on some custom dependency injection provided by Dependency.
-
Inputs directed at sysui (as opposed to general listeners) generally come in
through IStatusBar. Outputs from sysui are through a variety of private APIs to
the android platform all over.
## SystemUIApplication
-When SystemUIApplication starts up, it will start up the services listed in
-config_systemUIServiceComponents or config_systemUIServiceComponentsPerUser.
+When SystemUIApplication starts up, it instantiates a Dagger graph from which
+various pieces of the application are built.
-Each of these services extend SystemUI. SystemUI provides them with a Context
-and gives them callbacks for onConfigurationChanged (this historically was
-the main path for onConfigurationChanged, now also happens through
-ConfigurationController). They also receive a callback for onBootCompleted
+To support customization, SystemUIApplication relies on the AndroidManifest.xml
+having an `android.app.AppComponentFactory` specified. Specifically, it relies
+on an `AppComponentFactory` that subclases `SystemUIAppComponentFactoryBase`.
+Implementations of this abstract base class must override
+`#createSystemUIInitializer(Context)` which returns a `SystemUIInitializer`.
+`SystemUIInitializer` primary job in turn is to intialize and return the Dagger
+root component back to the `SystemUIApplication`.
+
+Writing a custom `SystemUIAppComponentFactoryBase` and `SystemUIInitializer`,
+should be enough for most implementations to stand up a customized Dagger
+graph, and launch a custom version of SystemUI.
+
+## Dagger / Dependency Injection
+
+See [dagger.md](docs/dagger.md) and https://dagger.dev/.
+
+## CoreStartable
+
+The starting point for most of SystemUI code is a list of classes that
+implement `CoreStartable` that are started up by SystemUIApplication.
+CoreStartables are like miniature services. They have their `#start` method
+called after being instantiated, and a reference to them is stored inside
+SystemUIApplication. They are in charge of their own behavior beyond this,
+registering and unregistering with the rest of the system as needed.
+
+`CoreStartable` also receives a callback for `#onBootCompleted`
since these objects may be started before the device has finished booting.
-Each SystemUI service is expected to be a major part of system ui and the
-goal is to minimize communication between them. So in general they should be
-relatively silo'd.
+`CoreStartable` is an ideal place to add new features and functionality
+that does not belong directly under the umbrella of an existing feature.
+It is better to define a new `CoreStartable` than to stick unrelated
+initialization code together in catch-all methods.
-## Dependencies
+CoreStartables are tied to application startup via Dagger:
-The first SystemUI service that is started should always be Dependency.
-Dependency provides a static method for getting a hold of dependencies that
-have a lifecycle that spans sysui. Dependency has code for how to create all
-dependencies manually added. SystemUIFactory is also capable of
-adding/replacing these dependencies.
+```kotlin
+class FeatureStartable
+@Inject
+constructor(
+ /* ... */
+) : CoreStartable {
+ override fun start() {
+ // ...
+ }
+}
-Dependencies are lazily initialized, so if a Dependency is never referenced at
-runtime, it will never be created.
+@Module
+abstract class FeatureModule {
+ @Binds
+ @IntoMap
+ @ClassKey(FeatureStartable::class)
+ abstract fun bind(impl: FeatureStartable): CoreStartable
+}
+```
-If an instantiated dependency implements Dumpable it will be included in dumps
-of sysui (and bug reports), allowing it to include current state information.
-This is how \*Controllers dump state to bug reports.
-
-If an instantiated dependency implements ConfigurationChangeReceiver it will
-receive onConfigurationChange callbacks when the configuration changes.
+Including `FeatureModule` in the Dagger graph such as this will ensure that
+`FeatureStartable` gets constructed and that its `#start` method is called.
## IStatusBar
@@ -64,12 +90,6 @@
This is generally used a shortcut to directly trigger CommandQueue rather than
calling StatusManager and waiting for the call to come back to IStatusBar.
-## Default SystemUI services list
-
-### [com.android.systemui.Dependency](/packages/SystemUI/src/com/android/systemui/Dependency.java)
-
-Provides custom dependency injection.
-
### [com.android.systemui.util.NotificationChannels](/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java)
Creates/initializes the channels sysui uses when posting notifications.
@@ -88,11 +108,11 @@
Registers all the callbacks/listeners required to show the Volume dialog when
it should be shown.
-### [com.android.systemui.status.phone.StatusBar](/packages/SystemUI/src/com/android/systemui/status/phone/StatusBar.java)
+### [com.android.systemui.status.phone.CentralSurfaces](/packages/SystemUI/src/com/android/systemui/status/phone/CentralSurfaces.java)
This shows the UI for the status bar and the notification shade it contains.
It also contains a significant amount of other UI that interacts with these
-surfaces (keyguard, AOD, etc.). StatusBar also contains a notification listener
+surfaces (keyguard, AOD, etc.). CentralSurfaces also contains a notification listener
to receive notification callbacks.
### [com.android.systemui.usb.StorageNotification](/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java)
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index cd45b8ea..60bfdb2 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -16,6 +16,9 @@
},
{
"exclude-annotation": "android.platform.test.annotations.Postsubmit"
+ },
+ {
+ "exclude-annotation": "android.platform.test.annotations.FlakyTest"
}
]
}
@@ -72,16 +75,16 @@
// Curious where your @Scenario tests will run?
//
// @Ignore: nowhere
- // @Staging or @FlakyTest: in staged-postsubmit, but not postsubmit or
- // presubmit
+ // @FlakyTest: in staged-postsubmit, but not blocking postsubmit or
+ // presubmit
// @Postsubmit: in postsubmit and staged-postsubmit, but not presubmit
// none of the above: in presubmit, postsubmit, and staged-postsubmit
//
- // Therefore, please annotate new tests with @Staging, then with @Postsubmit
- // once they're ready for postsubmit, then with neither once they're ready
- // for presubmit.
+ // Ideally, please annotate new tests with @FlakyTest, then with @Postsubmit
+ // once they're ready for postsubmit as they will immediately block go/android-platinum,
+ // then with neither once they're ready for presubmit.
//
- // If you don't use @Staging or @Postsubmit, your new test will immediately
+ // If you don't use @Postsubmit, your new test will immediately
// block presubmit, which is probably not what you want!
"sysui-platinum-postsubmit": [
{
@@ -98,6 +101,9 @@
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "android.platform.test.annotations.FlakyTest"
}
]
}
@@ -130,6 +136,9 @@
},
{
"include-filter": "android.platform.test.scenario.sysui"
+ },
+ {
+ "exclude-annotation": "android.platform.test.annotations.FlakyTest"
}
]
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index fe349f2..17a94b86 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -37,8 +37,11 @@
import android.view.WindowManager
import android.view.animation.Interpolator
import android.view.animation.PathInterpolator
+import androidx.annotation.BinderThread
+import androidx.annotation.UiThread
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
+import java.lang.IllegalArgumentException
import kotlin.math.roundToInt
private const val TAG = "ActivityLaunchAnimator"
@@ -226,7 +229,7 @@
// If we expect an animation, post a timeout to cancel it in case the remote animation is
// never started.
if (willAnimate) {
- runner.postTimeout()
+ runner.delegate.postTimeout()
// Hide the keyguard using the launch animation instead of the default unlock animation.
if (hideKeyguardWithAnimation) {
@@ -299,10 +302,13 @@
interface Callback {
/** Whether we are currently on the keyguard or not. */
- fun isOnKeyguard(): Boolean
+ @JvmDefault fun isOnKeyguard(): Boolean = false
/** Hide the keyguard and animate using [runner]. */
- fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner)
+ @JvmDefault
+ fun hideKeyguardWithAnimation(runner: IRemoteAnimationRunner) {
+ throw UnsupportedOperationException()
+ }
/* Get the background color of [task]. */
fun getBackgroundColor(task: TaskInfo): Int
@@ -333,13 +339,24 @@
* Return a [Controller] that will animate and expand [view] into the opening window.
*
* Important: The view must be attached to a [ViewGroup] when calling this function and
- * during the animation. For safety, this method will return null when it is not.
+ * during the animation. For safety, this method will return null when it is not. The
+ * view must also implement [LaunchableView], otherwise this method will throw.
*
* Note: The background of [view] should be a (rounded) rectangle so that it can be
* properly animated.
*/
@JvmStatic
fun fromView(view: View, cujType: Int? = null): Controller? {
+ // Make sure the View we launch from implements LaunchableView to avoid visibility
+ // issues.
+ if (view !is LaunchableView) {
+ throw IllegalArgumentException(
+ "An ActivityLaunchAnimator.Controller was created from a View that does " +
+ "not implement LaunchableView. This can lead to subtle bugs where the" +
+ " visibility of the View we are launching from is not what we expected."
+ )
+ }
+
if (view.parent !is ViewGroup) {
Log.e(
TAG,
@@ -389,14 +406,51 @@
fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean? = null) {}
}
- class Runner(
+ @VisibleForTesting
+ inner class Runner(
+ controller: Controller,
+ callback: Callback,
+ /** The animator to use to animate the window launch. */
+ launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ /** Listener for animation lifecycle events. */
+ listener: Listener? = null
+ ) : IRemoteAnimationRunner.Stub() {
+ private val context = controller.launchContainer.context
+ internal val delegate: AnimationDelegate
+
+ init {
+ delegate = AnimationDelegate(controller, callback, launchAnimator, listener)
+ }
+
+ @BinderThread
+ override fun onAnimationStart(
+ transit: Int,
+ apps: Array<out RemoteAnimationTarget>?,
+ wallpapers: Array<out RemoteAnimationTarget>?,
+ nonApps: Array<out RemoteAnimationTarget>?,
+ finishedCallback: IRemoteAnimationFinishedCallback?
+ ) {
+ context.mainExecutor.execute {
+ delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback)
+ }
+ }
+
+ @BinderThread
+ override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {
+ context.mainExecutor.execute { delegate.onAnimationCancelled(isKeyguardOccluded) }
+ }
+ }
+
+ class AnimationDelegate
+ @JvmOverloads
+ constructor(
private val controller: Controller,
private val callback: Callback,
/** The animator to use to animate the window launch. */
private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
/** Listener for animation lifecycle events. */
private val listener: Listener? = null
- ) : IRemoteAnimationRunner.Stub() {
+ ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
private val launchContainer = controller.launchContainer
private val context = launchContainer.context
private val transactionApplierView =
@@ -419,6 +473,7 @@
// posting it.
private var onTimeout = Runnable { onAnimationTimedOut() }
+ @UiThread
internal fun postTimeout() {
launchContainer.postDelayed(onTimeout, LAUNCH_TIMEOUT)
}
@@ -427,19 +482,20 @@
launchContainer.removeCallbacks(onTimeout)
}
+ @UiThread
override fun onAnimationStart(
@WindowManager.TransitionOldType transit: Int,
apps: Array<out RemoteAnimationTarget>?,
wallpapers: Array<out RemoteAnimationTarget>?,
nonApps: Array<out RemoteAnimationTarget>?,
- iCallback: IRemoteAnimationFinishedCallback?
+ callback: IRemoteAnimationFinishedCallback?
) {
removeTimeout()
// The animation was started too late and we already notified the controller that it
// timed out.
if (timedOut) {
- iCallback?.invoke()
+ callback?.invoke()
return
}
@@ -449,7 +505,7 @@
return
}
- context.mainExecutor.execute { startAnimation(apps, nonApps, iCallback) }
+ startAnimation(apps, nonApps, callback)
}
private fun startAnimation(
@@ -687,6 +743,7 @@
controller.onLaunchAnimationCancelled()
}
+ @UiThread
override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {
if (timedOut) {
return
@@ -695,10 +752,9 @@
Log.i(TAG, "Remote animation was cancelled")
cancelled = true
removeTimeout()
- context.mainExecutor.execute {
- animation?.cancel()
- controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded)
- }
+
+ animation?.cancel()
+ controller.onLaunchAnimationCancelled(newKeyguardOccludedState = isKeyguardOccluded)
}
private fun IRemoteAnimationFinishedCallback.invoke() {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
new file mode 100644
index 0000000..1c9dabb
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
@@ -0,0 +1,6 @@
+package com.android.systemui.animation
+
+interface AnimationFeatureFlags {
+ val isPredictiveBackQsDialogAnim: Boolean
+ get() = false
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 54aa351..b8d78fb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -33,8 +33,14 @@
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
import android.widget.FrameLayout
+import android.window.OnBackInvokedDispatcher
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CujType
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.applyTo
+import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
+import com.android.systemui.animation.back.onBackAnimationCallbackFrom
+import java.lang.IllegalArgumentException
import kotlin.math.roundToInt
private const val TAG = "DialogLaunchAnimator"
@@ -55,8 +61,9 @@
constructor(
private val callback: Callback,
private val interactionJankMonitor: InteractionJankMonitor,
+ private val featureFlags: AnimationFeatureFlags,
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
- private val isForTesting: Boolean = false
+ private val isForTesting: Boolean = false,
) {
private companion object {
private val TIMINGS = ActivityLaunchAnimator.TIMINGS
@@ -151,12 +158,23 @@
* Create a [Controller] that can animate [source] to and from a dialog.
*
* Important: The view must be attached to a [ViewGroup] when calling this function and
- * during the animation. For safety, this method will return null when it is not.
+ * during the animation. For safety, this method will return null when it is not. The
+ * view must also implement [LaunchableView], otherwise this method will throw.
*
* Note: The background of [view] should be a (rounded) rectangle so that it can be
* properly animated.
*/
fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
+ // Make sure the View we launch from implements LaunchableView to avoid visibility
+ // issues.
+ if (source !is LaunchableView) {
+ throw IllegalArgumentException(
+ "A DialogLaunchAnimator.Controller was created from a View that does not " +
+ "implement LaunchableView. This can lead to subtle bugs where the " +
+ "visibility of the View we are launching from is not what we expected."
+ )
+ }
+
if (source.parent !is ViewGroup) {
Log.e(
TAG,
@@ -237,25 +255,12 @@
openedDialogs.firstOrNull {
it.dialog.window.decorView.viewRootImpl == controller.viewRoot
}
- val animateFrom =
+ val controller =
animatedParent?.dialogContentWithBackground?.let {
Controller.fromView(it, controller.cuj)
}
?: controller
- if (animatedParent == null && animateFrom !is LaunchableView) {
- // Make sure the View we launch from implements LaunchableView to avoid visibility
- // issues. Given that we don't own dialog decorViews so we can't enforce it for launches
- // from a dialog.
- // TODO(b/243636422): Throw instead of logging to enforce this.
- Log.w(
- TAG,
- "A dialog was launched from a View that does not implement LaunchableView. This " +
- "can lead to subtle bugs where the visibility of the View we are " +
- "launching from is not what we expected."
- )
- }
-
// Make sure we don't run the launch animation from the same source twice at the same time.
if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
Log.e(
@@ -269,15 +274,16 @@
val animatedDialog =
AnimatedDialog(
- launchAnimator,
- callback,
- interactionJankMonitor,
- animateFrom,
+ launchAnimator = launchAnimator,
+ callback = callback,
+ interactionJankMonitor = interactionJankMonitor,
+ controller = controller,
onDialogDismissed = { openedDialogs.remove(it) },
dialog = dialog,
- animateBackgroundBoundsChange,
- animatedParent,
- isForTesting,
+ animateBackgroundBoundsChange = animateBackgroundBoundsChange,
+ parentAnimatedDialog = animatedParent,
+ forceDisableSynchronization = isForTesting,
+ featureFlags = featureFlags,
)
openedDialogs.add(animatedDialog)
@@ -366,7 +372,7 @@
val dialog = animatedDialog.dialog
// Don't animate if the dialog is not showing or if we are locked and going to show the
- // bouncer.
+ // primary bouncer.
if (
!dialog.isShowing ||
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
@@ -513,6 +519,7 @@
* Whether synchronization should be disabled, which can be useful if we are running in a test.
*/
private val forceDisableSynchronization: Boolean,
+ private val featureFlags: AnimationFeatureFlags,
) {
/**
* The DecorView of this dialog window.
@@ -601,10 +608,16 @@
}
// Animate that view with the background. Throw if we didn't find one, because
- // otherwise
- // it's not clear what we should animate.
+ // otherwise it's not clear what we should animate.
+ if (viewGroupWithBackground == null) {
+ error("Unable to find ViewGroup with background")
+ }
+
+ if (viewGroupWithBackground !is LaunchableView) {
+ error("The animated ViewGroup with background must implement LaunchableView")
+ }
+
viewGroupWithBackground
- ?: throw IllegalStateException("Unable to find ViewGroup with background")
} else {
// We will make the dialog window (and therefore its DecorView) fullscreen to make
// it possible to animate outside its bounds.
@@ -627,7 +640,7 @@
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
)
- val dialogContentWithBackground = FrameLayout(dialog.context)
+ val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
dialogContentWithBackground.background = decorView.background
// Make the window background transparent. Note that setting the window (or
@@ -708,7 +721,10 @@
// Make the background view invisible until we start the animation. We use the transition
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
- // b/204944038#comment17).
+ // b/204944038#comment17). Given that this background implements LaunchableView, we call
+ // setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is
+ // restored at the end of the animation.
+ dialogContentWithBackground.setShouldBlockVisibilityChanges(true)
dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
// Make sure the dialog is visible instantly and does not do any window animation.
@@ -774,12 +790,45 @@
// the dialog.
dialog.setDismissOverride(this::onDialogDismissed)
+ if (featureFlags.isPredictiveBackQsDialogAnim) {
+ // TODO(b/265923095) Improve animations for QS dialogs on configuration change
+ registerOnBackInvokedCallback(targetView = dialogContentWithBackground)
+ }
+
// Show the dialog.
dialog.show()
-
moveSourceDrawingToDialog()
}
+ private fun registerOnBackInvokedCallback(targetView: View) {
+ val metrics = targetView.resources.displayMetrics
+
+ val onBackAnimationCallback =
+ onBackAnimationCallbackFrom(
+ backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(metrics),
+ displayMetrics = metrics, // TODO(b/265060720): We could remove this
+ onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) },
+ onBackInvoked = { dialog.dismiss() },
+ )
+
+ val dispatcher = dialog.onBackInvokedDispatcher
+ targetView.addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ dispatcher.registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+ onBackAnimationCallback
+ )
+ }
+
+ override fun onViewDetachedFromWindow(v: View) {
+ targetView.removeOnAttachStateChangeListener(this)
+ dispatcher.unregisterOnBackInvokedCallback(onBackAnimationCallback)
+ }
+ }
+ )
+ }
+
private fun moveSourceDrawingToDialog() {
if (decorView.viewRootImpl == null) {
// Make sure that we have access to the dialog view root to move the drawing to the
@@ -791,13 +840,13 @@
// Move the drawing of the source in the overlay of this dialog, then animate. We trigger a
// one-off synchronization to make sure that this is done in sync between the two different
// windows.
+ controller.startDrawingInOverlayOf(decorView)
synchronizeNextDraw(
then = {
isSourceDrawnInDialog = true
maybeStartLaunchAnimation()
}
)
- controller.startDrawingInOverlayOf(decorView)
}
/**
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
index 3d341af..2903288 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
@@ -33,9 +33,7 @@
private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
private const val FONT_ITALIC_DEFAULT_VALUE = 0f
-/**
- * Provide interpolation of two fonts by adjusting font variation settings.
- */
+/** Provide interpolation of two fonts by adjusting font variation settings. */
class FontInterpolator {
/**
@@ -61,11 +59,14 @@
var index: Int,
val sortedAxes: MutableList<FontVariationAxis>
) {
- constructor(font: Font, axes: List<FontVariationAxis>) :
- this(font.sourceIdentifier,
- font.ttcIndex,
- axes.toMutableList().apply { sortBy { it.tag } }
- )
+ constructor(
+ font: Font,
+ axes: List<FontVariationAxis>
+ ) : this(
+ font.sourceIdentifier,
+ font.ttcIndex,
+ axes.toMutableList().apply { sortBy { it.tag } }
+ )
fun set(font: Font, axes: List<FontVariationAxis>) {
sourceId = font.sourceIdentifier
@@ -86,9 +87,7 @@
private val tmpInterpKey = InterpKey(null, null, 0f)
private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf())
- /**
- * Linear interpolate the font variation settings.
- */
+ /** Linear interpolate the font variation settings. */
fun lerp(start: Font, end: Font, progress: Float): Font {
if (progress == 0f) {
return start
@@ -115,27 +114,34 @@
// this doesn't take much time since the variation axes is usually up to 5. If we need to
// support more number of axes, we may want to preprocess the font and store the sorted axes
// and also pre-fill the missing axes value with default value from 'fvar' table.
- val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue ->
- when (tag) {
- // TODO: Good to parse 'fvar' table for retrieving default value.
- TAG_WGHT -> adjustWeight(
- MathUtils.lerp(
+ val newAxes =
+ lerp(startAxes, endAxes) { tag, startValue, endValue ->
+ when (tag) {
+ // TODO: Good to parse 'fvar' table for retrieving default value.
+ TAG_WGHT ->
+ adjustWeight(
+ MathUtils.lerp(
startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
- progress))
- TAG_ITAL -> adjustItalic(
- MathUtils.lerp(
+ progress
+ )
+ )
+ TAG_ITAL ->
+ adjustItalic(
+ MathUtils.lerp(
startValue ?: FONT_ITALIC_DEFAULT_VALUE,
endValue ?: FONT_ITALIC_DEFAULT_VALUE,
- progress))
- else -> {
- require(startValue != null && endValue != null) {
- "Unable to interpolate due to unknown default axes value : $tag"
+ progress
+ )
+ )
+ else -> {
+ require(startValue != null && endValue != null) {
+ "Unable to interpolate due to unknown default axes value : $tag"
+ }
+ MathUtils.lerp(startValue, endValue, progress)
}
- MathUtils.lerp(startValue, endValue, progress)
}
}
- }
// Check if we already make font for this axes. This is typically happens if the animation
// happens backward.
@@ -149,9 +155,7 @@
// This is the first time to make the font for the axes. Build and store it to the cache.
// Font.Builder#build won't throw IOException since creating fonts from existing fonts will
// not do any IO work.
- val newFont = Font.Builder(start)
- .setFontVariationSettings(newAxes.toTypedArray())
- .build()
+ val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
interpCache[InterpKey(start, end, progress)] = newFont
verFontCache[VarFontKey(start, newAxes)] = newFont
return newFont
@@ -173,26 +177,28 @@
val tagA = if (i < start.size) start[i].tag else null
val tagB = if (j < end.size) end[j].tag else null
- val comp = when {
- tagA == null -> 1
- tagB == null -> -1
- else -> tagA.compareTo(tagB)
- }
+ val comp =
+ when {
+ tagA == null -> 1
+ tagB == null -> -1
+ else -> tagA.compareTo(tagB)
+ }
- val axis = when {
- comp == 0 -> {
- val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue)
- FontVariationAxis(tagA, v)
+ val axis =
+ when {
+ comp == 0 -> {
+ val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue)
+ FontVariationAxis(tagA, v)
+ }
+ comp < 0 -> {
+ val v = filter(tagA!!, start[i++].styleValue, null)
+ FontVariationAxis(tagA, v)
+ }
+ else -> { // comp > 0
+ val v = filter(tagB!!, null, end[j++].styleValue)
+ FontVariationAxis(tagB, v)
+ }
}
- comp < 0 -> {
- val v = filter(tagA!!, start[i++].styleValue, null)
- FontVariationAxis(tagA, v)
- }
- else -> { // comp > 0
- val v = filter(tagB!!, null, end[j++].styleValue)
- FontVariationAxis(tagB, v)
- }
- }
result.add(axis)
}
@@ -202,21 +208,21 @@
// For the performance reasons, we animate weight with FONT_WEIGHT_ANIMATION_STEP. This helps
// Cache hit ratio in the Skia glyph cache.
private fun adjustWeight(value: Float) =
- coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP)
+ coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP)
// For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps
// Cache hit ratio in the Skia glyph cache.
private fun adjustItalic(value: Float) =
- coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP)
+ coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP)
private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) =
- (v.coerceIn(min, max) / step).toInt() * step
+ (v.coerceIn(min, max) / step).toInt() * step
companion object {
private val EMPTY_AXES = arrayOf<FontVariationAxis>()
// Returns true if given two font instance can be interpolated.
fun canInterpolate(start: Font, end: Font) =
- start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
+ start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index 0028d13..23e3a01 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -26,6 +26,7 @@
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.StateListDrawable
import android.util.Log
import android.view.GhostView
import android.view.View
@@ -33,8 +34,10 @@
import android.view.ViewGroupOverlay
import android.widget.FrameLayout
import com.android.internal.jank.InteractionJankMonitor
+import java.lang.IllegalArgumentException
import java.util.LinkedList
import kotlin.math.min
+import kotlin.math.roundToInt
private const val TAG = "GhostedViewLaunchAnimatorController"
@@ -44,12 +47,15 @@
* of the ghosted view.
*
* Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
- * the animation.
+ * the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
+ * during this controller instantiation.
*
* Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
* whenever possible instead.
*/
-open class GhostedViewLaunchAnimatorController @JvmOverloads constructor(
+open class GhostedViewLaunchAnimatorController
+@JvmOverloads
+constructor(
/** The view that will be ghosted and from which the background will be extracted. */
private val ghostedView: View,
@@ -97,6 +103,15 @@
private val background: Drawable?
init {
+ // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
+ if (ghostedView !is LaunchableView) {
+ throw IllegalArgumentException(
+ "A GhostedViewLaunchAnimatorController was created from a View that does not " +
+ "implement LaunchableView. This can lead to subtle bugs where the visibility " +
+ "of the View we are launching from is not what we expected."
+ )
+ }
+
/** Find the first view with a background in [view] and its children. */
fun findBackground(view: View): Drawable? {
if (view.background != null) {
@@ -145,7 +160,8 @@
val gradient = findGradientDrawable(drawable) ?: return 0f
// TODO(b/184121838): Support more than symmetric top & bottom radius.
- return gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
+ val radius = gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
+ return radius * ghostedView.scaleX
}
/** Return the current bottom corner radius of the background. */
@@ -154,7 +170,8 @@
val gradient = findGradientDrawable(drawable) ?: return 0f
// TODO(b/184121838): Support more than symmetric top & bottom radius.
- return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
+ val radius = gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
+ return radius * ghostedView.scaleX
}
override fun createAnimatorState(): LaunchAnimator.State {
@@ -173,9 +190,13 @@
ghostedView.getLocationOnScreen(ghostedViewLocation)
val insets = backgroundInsets
state.top = ghostedViewLocation[1] + insets.top
- state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom
+ state.bottom =
+ ghostedViewLocation[1] + (ghostedView.height * ghostedView.scaleY).roundToInt() -
+ insets.bottom
state.left = ghostedViewLocation[0] + insets.left
- state.right = ghostedViewLocation[0] + ghostedView.width - insets.right
+ state.right =
+ ghostedViewLocation[0] + (ghostedView.width * ghostedView.scaleX).roundToInt() -
+ insets.right
}
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -195,14 +216,16 @@
backgroundDrawable = WrappedDrawable(background)
backgroundView?.background = backgroundDrawable
+ // Delay the calls to `ghostedView.setVisibility()` during the animation. This must be
+ // called before `GhostView.addGhost()` is called because the latter will change the
+ // *transition* visibility, which won't be blocked and will affect the normal View
+ // visibility that is saved by `setShouldBlockVisibilityChanges()` for a later restoration.
+ (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+
// Create a ghost of the view that will be moving and fading out. This allows to fade out
// the content before fading out the background.
ghostView = GhostView.addGhost(ghostedView, launchContainer)
- // The ghost was just created, so ghostedView is currently invisible. We need to make sure
- // that it stays invisible as long as we are animating.
- (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
-
val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX
matrix.getValues(initialGhostViewMatrixValues)
@@ -297,14 +320,19 @@
backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
GhostView.removeGhost(ghostedView)
- (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
launchContainerOverlay.remove(backgroundView)
- // Make sure that the view is considered VISIBLE by accessibility by first making it
- // INVISIBLE then VISIBLE (see b/204944038#comment17 for more info).
- ghostedView.visibility = View.INVISIBLE
- ghostedView.visibility = View.VISIBLE
- ghostedView.invalidate()
+ if (ghostedView is LaunchableView) {
+ // Restore the ghosted view visibility.
+ ghostedView.setShouldBlockVisibilityChanges(false)
+ } else {
+ // Make the ghosted view visible. We ensure that the view is considered VISIBLE by
+ // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
+ // for more info).
+ ghostedView.visibility = View.INVISIBLE
+ ghostedView.visibility = View.VISIBLE
+ ghostedView.invalidate()
+ }
}
companion object {
@@ -334,6 +362,10 @@
}
}
+ if (drawable is StateListDrawable) {
+ return findGradientDrawable(drawable.current)
+ }
+
return null
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
new file mode 100644
index 0000000..2eb503b
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableFrameLayout.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+/** A [FrameLayout] that also implements [LaunchableView]. */
+open class LaunchableFrameLayout : FrameLayout, LaunchableView {
+ private val delegate =
+ LaunchableViewDelegate(
+ this,
+ superSetVisibility = { super.setVisibility(it) },
+ )
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {
+ delegate.setShouldBlockVisibilityChanges(block)
+ }
+
+ override fun setVisibility(visibility: Int) {
+ delegate.setVisibility(visibility)
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
index 67b59e0..774255b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt
@@ -21,15 +21,19 @@
/** A view that can expand/launch into an app or a dialog. */
interface LaunchableView {
/**
- * Set whether this view should block/postpone all visibility changes. This ensures that this
- * view:
+ * Set whether this view should block/postpone all calls to [View.setVisibility]. This ensures
+ * that this view:
* - remains invisible during the launch animation given that it is ghosted and already drawn
* somewhere else.
* - remains invisible as long as a dialog expanded from it is shown.
* - restores its expected visibility once the dialog expanded from it is dismissed.
*
- * Note that when this is set to true, both the [normal][android.view.View.setVisibility] and
- * [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked.
+ * When `setShouldBlockVisibilityChanges(false)` is called, then visibility of the View should
+ * be restored to its expected value, i.e. it should have the visibility of the last call to
+ * `View.setVisibility()` that was made after `setShouldBlockVisibilityChanges(true)`, if any,
+ * or the original view visibility otherwise.
+ *
+ * Note that calls to [View.setTransitionVisibility] shouldn't be blocked.
*
* @param block whether we should block/postpone all calls to `setVisibility` and
* `setTransitionVisibility`.
@@ -46,27 +50,31 @@
* super.setVisibility(visibility).
*/
private val superSetVisibility: (Int) -> Unit,
-
- /**
- * The lambda that should set the actual transition visibility of [view], usually by calling
- * super.setTransitionVisibility(visibility).
- */
- private val superSetTransitionVisibility: (Int) -> Unit,
-) {
+) : LaunchableView {
private var blockVisibilityChanges = false
private var lastVisibility = view.visibility
/** Call this when [LaunchableView.setShouldBlockVisibilityChanges] is called. */
- fun setShouldBlockVisibilityChanges(block: Boolean) {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {
if (block == blockVisibilityChanges) {
return
}
blockVisibilityChanges = block
if (block) {
+ // Save the current visibility for later.
lastVisibility = view.visibility
} else {
- superSetVisibility(lastVisibility)
+ // Restore the visibility. To avoid accessibility issues, we change the visibility twice
+ // which makes sure that we trigger a visibility flag change (see b/204944038#comment17
+ // for more info).
+ if (lastVisibility == View.VISIBLE) {
+ superSetVisibility(View.INVISIBLE)
+ superSetVisibility(View.VISIBLE)
+ } else {
+ superSetVisibility(View.VISIBLE)
+ superSetVisibility(lastVisibility)
+ }
}
}
@@ -79,16 +87,4 @@
superSetVisibility(visibility)
}
-
- /** Call this when [View.setTransitionVisibility] is called. */
- fun setTransitionVisibility(visibility: Int) {
- if (blockVisibilityChanges) {
- // View.setTransitionVisibility just sets the visibility flag, so we don't have to save
- // the transition visibility separately from the normal visibility.
- lastVisibility = visibility
- return
- }
-
- superSetTransitionVisibility(visibility)
- }
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
new file mode 100644
index 0000000..337408b
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationDelegate.kt
@@ -0,0 +1,30 @@
+package com.android.systemui.animation
+
+import android.annotation.UiThread
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.RemoteAnimationTarget
+import android.view.WindowManager
+
+/**
+ * A component capable of running remote animations.
+ *
+ * Expands the IRemoteAnimationRunner API by allowing for different types of more specialized
+ * callbacks.
+ */
+interface RemoteAnimationDelegate<in T : IRemoteAnimationFinishedCallback> {
+ /**
+ * Called on the UI thread when the animation targets are received. Sets up and kicks off the
+ * animation.
+ */
+ @UiThread
+ fun onAnimationStart(
+ @WindowManager.TransitionOldType transit: Int,
+ apps: Array<out RemoteAnimationTarget>?,
+ wallpapers: Array<out RemoteAnimationTarget>?,
+ nonApps: Array<out RemoteAnimationTarget>?,
+ callback: T?
+ )
+
+ /** Called on the UI thread when a signal is received to cancel the animation. */
+ @UiThread fun onAnimationCancelled(isKeyguardOccluded: Boolean)
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 5f1bb83..fdab749 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -36,8 +36,8 @@
* Currently this class can provide text style animation for text weight and text size. For example
* the simple view that draws text with animating text size is like as follows:
*
- * <pre>
- * <code>
+ * <pre> <code>
+ * ```
* class SimpleTextAnimation : View {
* @JvmOverloads constructor(...)
*
@@ -53,83 +53,63 @@
* animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate)
* }
* }
- * </code>
- * </pre>
+ * ```
+ * </code> </pre>
*/
-class TextAnimator(
- layout: Layout,
- private val invalidateCallback: () -> Unit
-) {
+class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
// Following two members are for mutable for testing purposes.
public var textInterpolator: TextInterpolator = TextInterpolator(layout)
- public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply {
- duration = DEFAULT_ANIMATION_DURATION
- addUpdateListener {
- textInterpolator.progress = it.animatedValue as Float
- invalidateCallback()
- }
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
- textInterpolator.rebase()
+ public var animator: ValueAnimator =
+ ValueAnimator.ofFloat(1f).apply {
+ duration = DEFAULT_ANIMATION_DURATION
+ addUpdateListener {
+ textInterpolator.progress = it.animatedValue as Float
+ invalidateCallback()
}
- override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
- })
- }
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ textInterpolator.rebase()
+ }
+ override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
+ }
+ )
+ }
sealed class PositionedGlyph {
- /**
- * Mutable X coordinate of the glyph position relative from drawing offset.
- */
+ /** Mutable X coordinate of the glyph position relative from drawing offset. */
var x: Float = 0f
- /**
- * Mutable Y coordinate of the glyph position relative from the baseline.
- */
+ /** Mutable Y coordinate of the glyph position relative from the baseline. */
var y: Float = 0f
- /**
- * The current line of text being drawn, in a multi-line TextView.
- */
+ /** The current line of text being drawn, in a multi-line TextView. */
var lineNo: Int = 0
- /**
- * Mutable text size of the glyph in pixels.
- */
+ /** Mutable text size of the glyph in pixels. */
var textSize: Float = 0f
- /**
- * Mutable color of the glyph.
- */
+ /** Mutable color of the glyph. */
var color: Int = 0
- /**
- * Immutable character offset in the text that the current font run start.
- */
+ /** Immutable character offset in the text that the current font run start. */
abstract var runStart: Int
protected set
- /**
- * Immutable run length of the font run.
- */
+ /** Immutable run length of the font run. */
abstract var runLength: Int
protected set
- /**
- * Immutable glyph index of the font run.
- */
+ /** Immutable glyph index of the font run. */
abstract var glyphIndex: Int
protected set
- /**
- * Immutable font instance for this font run.
- */
+ /** Immutable font instance for this font run. */
abstract var font: Font
protected set
- /**
- * Immutable glyph ID for this glyph.
- */
+ /** Immutable glyph ID for this glyph. */
abstract var glyphId: Int
protected set
}
@@ -147,20 +127,18 @@
/**
* GlyphFilter applied just before drawing to canvas for tweaking positions and text size.
*
- * This callback is called for each glyphs just before drawing the glyphs. This function will
- * be called with the intrinsic position, size, color, glyph ID and font instance. You can
- * mutate the position, size and color for tweaking animations.
- * Do not keep the reference of passed glyph object. The interpolator reuses that object for
- * avoiding object allocations.
+ * This callback is called for each glyphs just before drawing the glyphs. This function will be
+ * called with the intrinsic position, size, color, glyph ID and font instance. You can mutate
+ * the position, size and color for tweaking animations. Do not keep the reference of passed
+ * glyph object. The interpolator reuses that object for avoiding object allocations.
*
- * Details:
- * The text is drawn with font run units. The font run is a text segment that draws with the
- * same font. The {@code runStart} and {@code runLimit} is a range of the font run in the text
- * that current glyph is in. Once the font run is determined, the system will convert characters
- * into glyph IDs. The {@code glyphId} is the glyph identifier in the font and
- * {@code glyphIndex} is the offset of the converted glyph array. Please note that the
- * {@code glyphIndex} is not a character index, because the character will not be converted to
- * glyph one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be
+ * Details: The text is drawn with font run units. The font run is a text segment that draws
+ * with the same font. The {@code runStart} and {@code runLimit} is a range of the font run in
+ * the text that current glyph is in. Once the font run is determined, the system will convert
+ * characters into glyph IDs. The {@code glyphId} is the glyph identifier in the font and {@code
+ * glyphIndex} is the offset of the converted glyph array. Please note that the {@code
+ * glyphIndex} is not a character index, because the character will not be converted to glyph
+ * one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be
* composed from multiple characters.
*
* Here is an example of font runs: "fin. 終わり"
@@ -193,7 +171,9 @@
*/
var glyphFilter: GlyphCallback?
get() = textInterpolator.glyphFilter
- set(value) { textInterpolator.glyphFilter = value }
+ set(value) {
+ textInterpolator.glyphFilter = value
+ }
fun draw(c: Canvas) = textInterpolator.draw(c)
@@ -208,7 +188,7 @@
* @param weight an optional text weight.
* @param textSize an optional font size.
* @param colors an optional colors array that must be the same size as numLines passed to
- * the TextInterpolator
+ * the TextInterpolator
* @param animate an optional boolean indicating true for showing style transition as animation,
* false for immediate style transition. True by default.
* @param duration an optional animation duration in milliseconds. This is ignored if animate is
@@ -237,10 +217,11 @@
if (weight >= 0) {
// Paint#setFontVariationSettings creates Typeface instance from scratch. To reduce the
// memory impact, cache the typeface result.
- textInterpolator.targetPaint.typeface = typefaceCache.getOrElse(weight) {
- textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
- textInterpolator.targetPaint.typeface
- }
+ textInterpolator.targetPaint.typeface =
+ typefaceCache.getOrElse(weight) {
+ textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
+ textInterpolator.targetPaint.typeface
+ }
}
if (color != null) {
textInterpolator.targetPaint.color = color
@@ -249,22 +230,24 @@
if (animate) {
animator.startDelay = delay
- animator.duration = if (duration == -1L) {
- DEFAULT_ANIMATION_DURATION
- } else {
- duration
- }
+ animator.duration =
+ if (duration == -1L) {
+ DEFAULT_ANIMATION_DURATION
+ } else {
+ duration
+ }
interpolator?.let { animator.interpolator = it }
if (onAnimationEnd != null) {
- val listener = object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
- onAnimationEnd.run()
- animator.removeListener(this)
+ val listener =
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ onAnimationEnd.run()
+ animator.removeListener(this)
+ }
+ override fun onAnimationCancel(animation: Animator?) {
+ animator.removeListener(this)
+ }
}
- override fun onAnimationCancel(animation: Animator?) {
- animator.removeListener(this)
- }
- }
animator.addListener(listener)
}
animator.start()
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
index 0448c81..341784e 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
@@ -26,12 +26,8 @@
import com.android.internal.graphics.ColorUtils
import java.lang.Math.max
-/**
- * Provide text style linear interpolation for plain text.
- */
-class TextInterpolator(
- layout: Layout
-) {
+/** Provide text style linear interpolation for plain text. */
+class TextInterpolator(layout: Layout) {
/**
* Returns base paint used for interpolation.
@@ -64,12 +60,11 @@
var baseFont: Font,
var targetFont: Font
) {
- val length: Int get() = end - start
+ val length: Int
+ get() = end - start
}
- /**
- * A class represents text layout of a single run.
- */
+ /** A class represents text layout of a single run. */
private class Run(
val glyphIds: IntArray,
val baseX: FloatArray, // same length as glyphIds
@@ -79,12 +74,8 @@
val fontRuns: List<FontRun>
)
- /**
- * A class represents text layout of a single line.
- */
- private class Line(
- val runs: List<Run>
- )
+ /** A class represents text layout of a single line. */
+ private class Line(val runs: List<Run>)
private var lines = listOf<Line>()
private val fontInterpolator = FontInterpolator()
@@ -106,8 +97,8 @@
/**
* The layout used for drawing text.
*
- * Only non-styled text is supported. Even if the given layout is created from Spanned, the
- * span information is not used.
+ * Only non-styled text is supported. Even if the given layout is created from Spanned, the span
+ * information is not used.
*
* The paint objects used for interpolation are not changed by this method call.
*
@@ -122,6 +113,9 @@
shapeText(value)
}
+ var shapedText: String = ""
+ private set
+
init {
// shapeText needs to be called after all members are initialized.
shapeText(layout)
@@ -130,8 +124,8 @@
/**
* Recalculate internal text layout for interpolation.
*
- * Whenever the target paint is modified, call this method to recalculate internal
- * text layout used for interpolation.
+ * Whenever the target paint is modified, call this method to recalculate internal text layout
+ * used for interpolation.
*/
fun onTargetPaintModified() {
updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false)
@@ -140,8 +134,8 @@
/**
* Recalculate internal text layout for interpolation.
*
- * Whenever the base paint is modified, call this method to recalculate internal
- * text layout used for interpolation.
+ * Whenever the base paint is modified, call this method to recalculate internal text layout
+ * used for interpolation.
*/
fun onBasePaintModified() {
updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true)
@@ -152,11 +146,11 @@
*
* The text interpolator does not calculate all the text position by text shaper due to
* performance reasons. Instead, the text interpolator shape the start and end state and
- * calculate text position of the middle state by linear interpolation. Due to this trick,
- * the text positions of the middle state is likely different from the text shaper result.
- * So, if you want to start animation from the middle state, you will see the glyph jumps due to
- * this trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different
- * from text shape result of weight 550.
+ * calculate text position of the middle state by linear interpolation. Due to this trick, the
+ * text positions of the middle state is likely different from the text shaper result. So, if
+ * you want to start animation from the middle state, you will see the glyph jumps due to this
+ * trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different from
+ * text shape result of weight 550.
*
* After calling this method, do not call onBasePaintModified() since it reshape the text and
* update the base state. As in above notice, the text shaping result at current progress is
@@ -168,8 +162,8 @@
* animate weight from 200 to 400, then if you want to move back to 200 at the half of the
* animation, it will look like
*
- * <pre>
- * <code>
+ * <pre> <code>
+ * ```
* val interp = TextInterpolator(layout)
*
* // Interpolate between weight 200 to 400.
@@ -199,9 +193,8 @@
* // progress is 0.5
* animator.start()
* }
- * </code>
- * </pre>
- *
+ * ```
+ * </code> </pre>
*/
fun rebase() {
if (progress == 0f) {
@@ -263,69 +256,73 @@
}
var maxRunLength = 0
- lines = baseLayout.zip(targetLayout) { baseLine, targetLine ->
- val runs = baseLine.zip(targetLine) { base, target ->
-
- require(base.glyphCount() == target.glyphCount()) {
- "Inconsistent glyph count at line ${lines.size}"
- }
-
- val glyphCount = base.glyphCount()
-
- // Good to recycle the array if the existing array can hold the new layout result.
- val glyphIds = IntArray(glyphCount) {
- base.getGlyphId(it).also { baseGlyphId ->
- require(baseGlyphId == target.getGlyphId(it)) {
- "Inconsistent glyph ID at $it in line ${lines.size}"
+ lines =
+ baseLayout.zip(targetLayout) { baseLine, targetLine ->
+ val runs =
+ baseLine.zip(targetLine) { base, target ->
+ require(base.glyphCount() == target.glyphCount()) {
+ "Inconsistent glyph count at line ${lines.size}"
}
- }
- }
- val baseX = FloatArray(glyphCount) { base.getGlyphX(it) }
- val baseY = FloatArray(glyphCount) { base.getGlyphY(it) }
- val targetX = FloatArray(glyphCount) { target.getGlyphX(it) }
- val targetY = FloatArray(glyphCount) { target.getGlyphY(it) }
+ val glyphCount = base.glyphCount()
- // Calculate font runs
- val fontRun = mutableListOf<FontRun>()
- if (glyphCount != 0) {
- var start = 0
- var baseFont = base.getFont(start)
- var targetFont = target.getFont(start)
- require(FontInterpolator.canInterpolate(baseFont, targetFont)) {
- "Cannot interpolate font at $start ($baseFont vs $targetFont)"
- }
-
- for (i in 1 until glyphCount) {
- val nextBaseFont = base.getFont(i)
- val nextTargetFont = target.getFont(i)
-
- if (baseFont !== nextBaseFont) {
- require(targetFont !== nextTargetFont) {
- "Base font has changed at $i but target font has not changed."
+ // Good to recycle the array if the existing array can hold the new layout
+ // result.
+ val glyphIds =
+ IntArray(glyphCount) {
+ base.getGlyphId(it).also { baseGlyphId ->
+ require(baseGlyphId == target.getGlyphId(it)) {
+ "Inconsistent glyph ID at $it in line ${lines.size}"
+ }
+ }
}
- // Font transition point. push run and reset context.
- fontRun.add(FontRun(start, i, baseFont, targetFont))
- maxRunLength = max(maxRunLength, i - start)
- baseFont = nextBaseFont
- targetFont = nextTargetFont
- start = i
+
+ val baseX = FloatArray(glyphCount) { base.getGlyphX(it) }
+ val baseY = FloatArray(glyphCount) { base.getGlyphY(it) }
+ val targetX = FloatArray(glyphCount) { target.getGlyphX(it) }
+ val targetY = FloatArray(glyphCount) { target.getGlyphY(it) }
+
+ // Calculate font runs
+ val fontRun = mutableListOf<FontRun>()
+ if (glyphCount != 0) {
+ var start = 0
+ var baseFont = base.getFont(start)
+ var targetFont = target.getFont(start)
require(FontInterpolator.canInterpolate(baseFont, targetFont)) {
"Cannot interpolate font at $start ($baseFont vs $targetFont)"
}
- } else { // baseFont === nextBaseFont
- require(targetFont === nextTargetFont) {
- "Base font has not changed at $i but target font has changed."
+
+ for (i in 1 until glyphCount) {
+ val nextBaseFont = base.getFont(i)
+ val nextTargetFont = target.getFont(i)
+
+ if (baseFont !== nextBaseFont) {
+ require(targetFont !== nextTargetFont) {
+ "Base font has changed at $i but target font is unchanged."
+ }
+ // Font transition point. push run and reset context.
+ fontRun.add(FontRun(start, i, baseFont, targetFont))
+ maxRunLength = max(maxRunLength, i - start)
+ baseFont = nextBaseFont
+ targetFont = nextTargetFont
+ start = i
+ require(FontInterpolator.canInterpolate(baseFont, targetFont)) {
+ "Cannot interpolate font at $start" +
+ " ($baseFont vs $targetFont)"
+ }
+ } else { // baseFont === nextBaseFont
+ require(targetFont === nextTargetFont) {
+ "Base font is unchanged at $i but target font has changed."
+ }
+ }
}
+ fontRun.add(FontRun(start, glyphCount, baseFont, targetFont))
+ maxRunLength = max(maxRunLength, glyphCount - start)
}
+ Run(glyphIds, baseX, baseY, targetX, targetY, fontRun)
}
- fontRun.add(FontRun(start, glyphCount, baseFont, targetFont))
- maxRunLength = max(maxRunLength, glyphCount - start)
- }
- Run(glyphIds, baseX, baseY, targetX, targetY, fontRun)
+ Line(runs)
}
- Line(runs)
- }
// Update float array used for drawing.
if (tmpPositionArray.size < maxRunLength * 2) {
@@ -357,9 +354,9 @@
if (glyphFilter == null) {
for (i in run.start until run.end) {
tmpPositionArray[arrayIndex++] =
- MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
+ MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
tmpPositionArray[arrayIndex++] =
- MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
+ MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
}
c.drawGlyphs(line.glyphIds, run.start, tmpPositionArray, 0, run.length, font, paint)
return
@@ -388,13 +385,14 @@
tmpPaintForGlyph.color = tmpGlyph.color
c.drawGlyphs(
- line.glyphIds,
- prevStart,
- tmpPositionArray,
- 0,
- i - prevStart,
- font,
- tmpPaintForGlyph)
+ line.glyphIds,
+ prevStart,
+ tmpPositionArray,
+ 0,
+ i - prevStart,
+ font,
+ tmpPaintForGlyph
+ )
prevStart = i
arrayIndex = 0
}
@@ -404,13 +402,14 @@
}
c.drawGlyphs(
- line.glyphIds,
- prevStart,
- tmpPositionArray,
- 0,
- run.end - prevStart,
- font,
- tmpPaintForGlyph)
+ line.glyphIds,
+ prevStart,
+ tmpPositionArray,
+ 0,
+ run.end - prevStart,
+ font,
+ tmpPaintForGlyph
+ )
}
private fun updatePositionsAndFonts(
@@ -418,9 +417,7 @@
updateBase: Boolean
) {
// Update target positions with newly calculated text layout.
- check(layoutResult.size == lines.size) {
- "The new layout result has different line count."
- }
+ check(layoutResult.size == lines.size) { "The new layout result has different line count." }
lines.zip(layoutResult) { line, runs ->
line.runs.zip(runs) { lineRun, newGlyphs ->
@@ -436,7 +433,7 @@
}
require(newFont === newGlyphs.getFont(i)) {
"The new layout has different font run." +
- " $newFont vs ${newGlyphs.getFont(i)} at $i"
+ " $newFont vs ${newGlyphs.getFont(i)} at $i"
}
}
@@ -444,7 +441,7 @@
// check new font can be interpolatable with base font.
require(FontInterpolator.canInterpolate(newFont, run.baseFont)) {
"New font cannot be interpolated with existing font. $newFont," +
- " ${run.baseFont}"
+ " ${run.baseFont}"
}
if (updateBase) {
@@ -480,14 +477,13 @@
}
// Shape the text and stores the result to out argument.
- private fun shapeText(
- layout: Layout,
- paint: TextPaint
- ): List<List<PositionedGlyphs>> {
+ private fun shapeText(layout: Layout, paint: TextPaint): List<List<PositionedGlyphs>> {
+ var text = StringBuilder()
val out = mutableListOf<List<PositionedGlyphs>>()
for (lineNo in 0 until layout.lineCount) { // Shape all lines.
val lineStart = layout.getLineStart(lineNo)
- var count = layout.getLineEnd(lineNo) - lineStart
+ val lineEnd = layout.getLineEnd(lineNo)
+ var count = lineEnd - lineStart
// Do not render the last character in the line if it's a newline and unprintable
val last = lineStart + count - 1
if (last > lineStart && last < layout.text.length && layout.text[last] == '\n') {
@@ -495,19 +491,28 @@
}
val runs = mutableListOf<PositionedGlyphs>()
- TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic,
- paint) { _, _, glyphs, _ ->
- runs.add(glyphs)
- }
+ TextShaper.shapeText(
+ layout.text,
+ lineStart,
+ count,
+ layout.textDirectionHeuristic,
+ paint
+ ) { _, _, glyphs, _ -> runs.add(glyphs) }
out.add(runs)
+
+ if (lineNo > 0) {
+ text.append("\n")
+ }
+ text.append(layout.text.substring(lineStart, lineEnd))
}
+ shapedText = text.toString()
return out
}
}
private fun Layout.getDrawOrigin(lineNo: Int) =
- if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) {
- getLineLeft(lineNo)
- } else {
- getLineRight(lineNo)
- }
+ if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) {
+ getLineLeft(lineNo)
+ } else {
+ getLineRight(lineNo)
+ }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
index 964ef8c..46d5a5c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
@@ -34,23 +34,29 @@
override val sourceIdentity: Any = source
override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+ // Delay the calls to `source.setVisibility()` during the animation. This must be called
+ // before `GhostView.addGhost()` is called because the latter will change the *transition*
+ // visibility, which won't be blocked and will affect the normal View visibility that is
+ // saved by `setShouldBlockVisibilityChanges()` for a later restoration.
+ (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+
// Create a temporary ghost of the source (which will make it invisible) and add it
// to the host dialog.
GhostView.addGhost(source, viewGroup)
-
- // The ghost of the source was just created, so the source is currently invisible.
- // We need to make sure that it stays invisible as long as the dialog is shown or
- // animating.
- (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
}
override fun stopDrawingInOverlay() {
// Note: here we should remove the ghost from the overlay, but in practice this is
- // already done by the launch controllers created below.
+ // already done by the launch controller created below.
- // Make sure we allow the source to change its visibility again.
- (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
- source.visibility = View.VISIBLE
+ if (source is LaunchableView) {
+ // Make sure we allow the source to change its visibility again and restore its previous
+ // value.
+ source.setShouldBlockVisibilityChanges(false)
+ } else {
+ // We made the source invisible earlier, so let's make it visible again.
+ source.visibility = View.VISIBLE
+ }
}
override fun createLaunchController(): LaunchAnimator.Controller {
@@ -67,10 +73,14 @@
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
- // We hide the source when the dialog is showing. We will make this view
- // visible again when dismissing the dialog. This does nothing if the source
- // implements [LaunchableView], as it's already INVISIBLE in that case.
- source.visibility = View.INVISIBLE
+ // At this point the view visibility is restored by the delegate, so we delay the
+ // visibility changes again and make it invisible while the dialog is shown.
+ if (source is LaunchableView) {
+ source.setShouldBlockVisibilityChanges(true)
+ source.setTransitionVisibility(View.INVISIBLE)
+ } else {
+ source.visibility = View.INVISIBLE
+ }
}
}
}
@@ -90,13 +100,15 @@
}
override fun onExitAnimationCancelled() {
- // Make sure we allow the source to change its visibility again.
- (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
- // If the view is invisible it's probably because of us, so we make it visible
- // again.
- if (source.visibility == View.INVISIBLE) {
- source.visibility = View.VISIBLE
+ if (source is LaunchableView) {
+ // Make sure we allow the source to change its visibility again.
+ source.setShouldBlockVisibilityChanges(false)
+ } else {
+ // If the view is invisible it's probably because of us, so we make it visible
+ // again.
+ if (source.visibility == View.INVISIBLE) {
+ source.visibility = View.VISIBLE
+ }
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
new file mode 100644
index 0000000..f3d8b17
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.animation.back
+
+import android.util.DisplayMetrics
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.util.dpToPx
+
+/** Used to convert [BackEvent] into a [BackTransformation]. */
+fun interface BackAnimationSpec {
+
+ /** Computes transformation based on a [backEvent] and sets it to [result]. */
+ fun getBackTransformation(
+ backEvent: BackEvent,
+ progressY: Float, // TODO(b/265060720): Remove progressY. Could be retrieved from backEvent
+ result: BackTransformation,
+ )
+
+ companion object
+}
+
+/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */
+fun BackAnimationSpec.Companion.createFloatingSurfaceAnimationSpec(
+ displayMetrics: DisplayMetrics,
+ maxMarginXdp: Float,
+ maxMarginYdp: Float,
+ minScale: Float,
+ translateXEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
+ translateYEasing: Interpolator = Interpolators.LINEAR,
+ scaleEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
+): BackAnimationSpec {
+ val screenWidthPx = displayMetrics.widthPixels
+ val screenHeightPx = displayMetrics.heightPixels
+
+ val maxMarginXPx = maxMarginXdp.dpToPx(displayMetrics)
+ val maxMarginYPx = maxMarginYdp.dpToPx(displayMetrics)
+ val maxTranslationXByScale = (screenWidthPx - screenWidthPx * minScale) / 2
+ val maxTranslationX = maxTranslationXByScale - maxMarginXPx
+ val maxTranslationYByScale = (screenHeightPx - screenHeightPx * minScale) / 2
+ val maxTranslationY = maxTranslationYByScale - maxMarginYPx
+ val minScaleReversed = 1f - minScale
+
+ return BackAnimationSpec { backEvent, progressY, result ->
+ val direction = if (backEvent.swipeEdge == BackEvent.EDGE_LEFT) 1 else -1
+ val progressX = backEvent.progress
+
+ val ratioTranslateX = translateXEasing.getInterpolation(progressX)
+ val ratioTranslateY = translateYEasing.getInterpolation(progressY)
+ val ratioScale = scaleEasing.getInterpolation(progressX)
+
+ result.apply {
+ translateX = ratioTranslateX * direction * maxTranslationX
+ translateY = ratioTranslateY * maxTranslationY
+ scale = 1f - (ratioScale * minScaleReversed)
+ }
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
new file mode 100644
index 0000000..c6b7073
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.animation.back
+
+import android.util.DisplayMetrics
+
+/**
+ * SysUI transitions - Dismiss app (ST1) Return to launching surface or place of origin
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-1-dismiss-app
+ */
+fun BackAnimationSpec.Companion.dismissAppForSysUi(
+ displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+ BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+ displayMetrics = displayMetrics,
+ maxMarginXdp = 8f,
+ maxMarginYdp = 8f,
+ minScale = 0.8f,
+ )
+
+/**
+ * SysUI transitions - Cross task (ST2) Return to previous task/app, keeping the current one open
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-2-cross-task
+ */
+fun BackAnimationSpec.Companion.crossTaskForSysUi(
+ displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+ BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+ displayMetrics = displayMetrics,
+ maxMarginXdp = 8f,
+ maxMarginYdp = 8f,
+ minScale = 0.8f,
+ )
+
+/**
+ * SysUI transitions - Inner area dismiss (ST3) Dismiss non-detachable surface
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-3-inner-area-dismiss
+ */
+fun BackAnimationSpec.Companion.innerAreaDismissForSysUi(
+ displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+ BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+ displayMetrics = displayMetrics,
+ maxMarginXdp = 0f,
+ maxMarginYdp = 0f,
+ minScale = 0.9f,
+ )
+
+/**
+ * SysUI transitions - Floating system surfaces (ST4)
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-4-floating-system-surfaces
+ */
+fun BackAnimationSpec.Companion.floatingSystemSurfacesForSysUi(
+ displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+ BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+ displayMetrics = displayMetrics,
+ maxMarginXdp = 8f,
+ maxMarginYdp = 8f,
+ minScale = 0.8f,
+ )
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
new file mode 100644
index 0000000..49d1fb4
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.animation.back
+
+import android.view.View
+
+/**
+ * This object that represents the transformation to apply to the target. The properties of this
+ * object are mutable for performance reasons (avoid recreating this object)
+ */
+data class BackTransformation(
+ var translateX: Float = Float.NaN,
+ var translateY: Float = Float.NaN,
+ var scale: Float = Float.NaN,
+)
+
+/** Apply the transformation to the [targetView] */
+fun BackTransformation.applyTo(targetView: View) {
+ if (translateX.isFinite()) targetView.translationX = translateX
+ if (translateY.isFinite()) targetView.translationY = translateY
+ if (scale.isFinite()) {
+ targetView.scaleX = scale
+ targetView.scaleY = scale
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
new file mode 100644
index 0000000..33d14b1
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import android.window.OnBackAnimationCallback
+
+/**
+ * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be
+ * called on each update passing the current [BackTransformation].
+ *
+ * Optionally, you can specify [onBackStarted], [onBackInvoked], and [onBackCancelled] callbacks.
+ */
+fun onBackAnimationCallbackFrom(
+ backAnimationSpec: BackAnimationSpec,
+ displayMetrics: DisplayMetrics, // TODO(b/265060720): We could remove this
+ onBackProgressed: (BackTransformation) -> Unit,
+ onBackStarted: (BackEvent) -> Unit = {},
+ onBackInvoked: () -> Unit = {},
+ onBackCancelled: () -> Unit = {},
+): OnBackAnimationCallback {
+ return object : OnBackAnimationCallback {
+ private var initialY = 0f
+ private val lastTransformation = BackTransformation()
+
+ override fun onBackStarted(backEvent: BackEvent) {
+ initialY = backEvent.touchY
+ onBackStarted(backEvent)
+ }
+
+ override fun onBackProgressed(backEvent: BackEvent) {
+ val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels
+
+ backAnimationSpec.getBackTransformation(
+ backEvent = backEvent,
+ progressY = progressY,
+ result = lastTransformation,
+ )
+
+ onBackProgressed(lastTransformation)
+ }
+
+ override fun onBackInvoked() {
+ onBackInvoked()
+ }
+
+ override fun onBackCancelled() {
+ onBackCancelled()
+ }
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
index 0e3d41c..7897934 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
@@ -48,7 +48,7 @@
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
- rippleShader.progress = progress
+ rippleShader.rawProgress = progress
rippleShader.distortionStrength = if (config.shouldDistort) 1 - progress else 0f
rippleShader.time = now.toFloat()
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index 9058510..4322d53 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -18,6 +18,7 @@
import android.graphics.PointF
import android.graphics.RuntimeShader
import android.util.MathUtils
+import com.android.systemui.animation.Interpolators
import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary
import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
@@ -47,7 +48,6 @@
"""
uniform vec2 in_center;
uniform vec2 in_size;
- uniform float in_progress;
uniform float in_cornerRadius;
uniform float in_thickness;
uniform float in_time;
@@ -162,21 +162,15 @@
maxSize.y = height
}
- /** Progress of the ripple. Float value between [0, 1]. */
- var progress: Float = 0.0f
+ /**
+ * Linear progress of the ripple. Float value between [0, 1].
+ *
+ * <p>Note that the progress here is expected to be linear without any curve applied.
+ */
+ var rawProgress: Float = 0.0f
set(value) {
field = value
- setFloatUniform("in_progress", value)
- val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
-
- currentWidth = maxSize.x * curvedProg
- currentHeight = maxSize.y * curvedProg
- setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
- setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
- // radius should not exceed width and height values.
- setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
-
- setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
+ progress = Interpolators.STANDARD.getInterpolation(value)
val fadeIn = subProgress(0f, 0.1f, value)
val fadeOutNoise = subProgress(0.4f, 1f, value)
@@ -191,6 +185,20 @@
setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
}
+ /** Progress with Standard easing curve applied. */
+ private var progress: Float = 0.0f
+ set(value) {
+ currentWidth = maxSize.x * value
+ currentHeight = maxSize.y * value
+ setFloatUniform("in_size", currentWidth, currentHeight)
+
+ setFloatUniform("in_thickness", maxSize.y * value * 0.5f)
+ // radius should not exceed width and height values.
+ setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * value)
+
+ setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
+ }
+
/** Play time since the start of the effect. */
var time: Float = 0.0f
set(value) {
@@ -220,7 +228,7 @@
var distortionStrength: Float = 0.0f
set(value) {
field = value
- setFloatUniform("in_distort_radial", 75 * progress * value)
+ setFloatUniform("in_distort_radial", 75 * rawProgress * value)
setFloatUniform("in_distort_xy", 75 * value)
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index b37c734..bc4796a 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -77,7 +77,7 @@
rippleShader = RippleShader(rippleShape)
rippleShader.color = RippleAnimationConfig.RIPPLE_DEFAULT_COLOR
- rippleShader.progress = 0f
+ rippleShader.rawProgress = 0f
rippleShader.sparkleStrength = RippleAnimationConfig.RIPPLE_SPARKLE_STRENGTH
rippleShader.pixelDensity = resources.displayMetrics.density
@@ -93,7 +93,7 @@
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
- rippleShader.progress = progress
+ rippleShader.rawProgress = progress
rippleShader.distortionStrength = 1 - progress
rippleShader.time = now.toFloat()
invalidate()
@@ -142,25 +142,13 @@
}
// To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
// active effect area. Values here should be kept in sync with the animation implementation
- // in the ripple shader.
+ // in the ripple shader. (Twice bigger)
if (rippleShape == RippleShape.CIRCLE) {
- val maskRadius =
- (1 -
- (1 - rippleShader.progress) *
- (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * maxWidth
+ val maskRadius = rippleShader.currentWidth
canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
} else {
- val maskWidth =
- (1 -
- (1 - rippleShader.progress) *
- (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * maxWidth * 2
- val maskHeight =
- (1 -
- (1 - rippleShader.progress) *
- (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * maxHeight * 2
+ val maskWidth = rippleShader.currentWidth * 2
+ val maskHeight = rippleShader.currentHeight * 2
canvas.drawRect(
/* left= */ centerX - maskWidth,
/* top= */ centerY - maskHeight,
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
index 6715951..79bc2f4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
@@ -57,7 +57,7 @@
val onAnimationEnd: Runnable? = null
) {
companion object {
- const val DEFAULT_MAX_DURATION_IN_MILLIS = 7500f
+ const val DEFAULT_MAX_DURATION_IN_MILLIS = 30_000f // Max 30 sec
const val DEFAULT_EASING_DURATION_IN_MILLIS = 750f
const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f
const val DEFAULT_NOISE_GRID_COUNT = 1.2f
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
new file mode 100644
index 0000000..4bc9972
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.res.Resources
+import android.util.DisplayMetrics
+import android.util.TypedValue
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(context: Context): Float = dpToPx(resources = context.resources)
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(resources: Resources): Float = dpToPx(displayMetrics = resources.displayMetrics)
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(displayMetrics: DisplayMetrics): Float =
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, toFloat(), displayMetrics)
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt
new file mode 100644
index 0000000..1f6e603
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetector.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.systemui.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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 org.jetbrains.uast.UElement
+import org.jetbrains.uast.UFile
+import org.jetbrains.uast.UImportStatement
+
+/**
+ * Detects violations of the Dependency Rule of Clean Architecture.
+ *
+ * The rule states that code in each layer may only depend on code in the same layer or the layer
+ * directly "beneath" that layer in the layer diagram.
+ *
+ * In System UI, we have three layers; from top to bottom, they are: ui, domain, and data. As a
+ * convention, was used packages with those names to place code in the appropriate layer. We also
+ * make an exception and allow for shared models to live under a separate package named "shared" to
+ * avoid code duplication.
+ *
+ * For more information, please see go/sysui-arch.
+ */
+@Suppress("UnstableApiUsage")
+class CleanArchitectureDependencyViolationDetector : Detector(), Detector.UastScanner {
+ override fun getApplicableUastTypes(): List<Class<out UElement>> {
+ return listOf(UFile::class.java)
+ }
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return object : UElementHandler() {
+ override fun visitFile(node: UFile) {
+ // Check which Clean Architecture layer this file belongs to:
+ matchingLayer(node.packageName)?.let { layer ->
+ // The file matches with a Clean Architecture layer. Let's check all of its
+ // imports.
+ node.imports.forEach { importStatement ->
+ visitImportStatement(context, layer, importStatement)
+ }
+ }
+ }
+ }
+ }
+
+ private fun visitImportStatement(
+ context: JavaContext,
+ layer: Layer,
+ importStatement: UImportStatement,
+ ) {
+ val importText = importStatement.importReference?.asSourceString() ?: return
+ val importedLayer = matchingLayer(importText) ?: return
+
+ // Now check whether the layer of the file may depend on the layer of the import.
+ if (!layer.mayDependOn(importedLayer)) {
+ context.report(
+ issue = ISSUE,
+ scope = importStatement,
+ location = context.getLocation(importStatement),
+ message =
+ "The ${layer.packageNamePart} layer may not depend on" +
+ " the ${importedLayer.packageNamePart} layer.",
+ )
+ }
+ }
+
+ private fun matchingLayer(packageName: String): Layer? {
+ val packageNameParts = packageName.split(".").toSet()
+ return Layer.values()
+ .filter { layer -> packageNameParts.contains(layer.packageNamePart) }
+ .takeIf { it.size == 1 }
+ ?.first()
+ }
+
+ private enum class Layer(
+ val packageNamePart: String,
+ val canDependOn: Set<Layer>,
+ ) {
+ SHARED(
+ packageNamePart = "shared",
+ canDependOn = emptySet(), // The shared layer may not depend on any other layer.
+ ),
+ DATA(
+ packageNamePart = "data",
+ canDependOn = setOf(SHARED),
+ ),
+ DOMAIN(
+ packageNamePart = "domain",
+ canDependOn = setOf(SHARED, DATA),
+ ),
+ UI(
+ packageNamePart = "ui",
+ canDependOn = setOf(DOMAIN, SHARED),
+ ),
+ ;
+
+ fun mayDependOn(otherLayer: Layer): Boolean {
+ return this == otherLayer || canDependOn.contains(otherLayer)
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ val ISSUE =
+ Issue.create(
+ id = "CleanArchitectureDependencyViolation",
+ briefDescription = "Violation of the Clean Architecture Dependency Rule.",
+ explanation =
+ """
+ Following the \"Dependency Rule\" from Clean Architecture, every layer of code \
+ can only depend code in its own layer or code in the layer directly \
+ \"beneath\" it. Therefore, the UI layer can only depend on the" Domain layer \
+ and the Domain layer can only depend on the Data layer. We" do make an \
+ exception to allow shared models to exist and be shared across layers by \
+ placing them under shared/model, which should be done with care. For more \
+ information about Clean Architecture in System UI, please see go/sysui-arch. \
+ NOTE: if your code is not using Clean Architecture, please feel free to ignore \
+ this warning.
+ """,
+ category = Category.CORRECTNESS,
+ priority = 8,
+ severity = Severity.WARNING,
+ implementation =
+ Implementation(
+ CleanArchitectureDependencyViolationDetector::class.java,
+ Scope.JAVA_FILE_SCOPE,
+ ),
+ )
+ }
+}
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 3f334c1c..254a6fb 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
@@ -27,9 +27,11 @@
class SystemUIIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
- get() = listOf(
+ get() =
+ listOf(
BindServiceOnMainThreadDetector.ISSUE,
BroadcastSentViaContextDetector.ISSUE,
+ CleanArchitectureDependencyViolationDetector.ISSUE,
SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY,
SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY,
NonInjectedMainThreadDetector.ISSUE,
@@ -37,7 +39,7 @@
SoftwareBitmapDetector.ISSUE,
NonInjectedServiceDetector.ISSUE,
StaticSettingsProviderDetector.ISSUE
- )
+ )
override val api: Int
get() = CURRENT_API
@@ -45,9 +47,9 @@
get() = 8
override val vendor: Vendor =
- Vendor(
- vendorName = "Android",
- feedbackUrl = "http://b/issues/new?component=78010",
- contact = "jernej@google.com"
- )
+ Vendor(
+ vendorName = "Android",
+ feedbackUrl = "http://b/issues/new?component=78010",
+ contact = "jernej@google.com"
+ )
}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt
new file mode 100644
index 0000000..a4b59fd
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/CleanArchitectureDependencyViolationDetectorTest.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+@Ignore("b/254533331")
+class CleanArchitectureDependencyViolationDetectorTest : SystemUILintDetectorTest() {
+ override fun getDetector(): Detector {
+ return CleanArchitectureDependencyViolationDetector()
+ }
+
+ override fun getIssues(): List<Issue> {
+ return listOf(
+ CleanArchitectureDependencyViolationDetector.ISSUE,
+ )
+ }
+
+ @Test
+ fun `No violations`() {
+ lint()
+ .files(
+ *LEGITIMATE_FILES,
+ )
+ .issues(
+ CleanArchitectureDependencyViolationDetector.ISSUE,
+ )
+ .run()
+ .expectWarningCount(0)
+ }
+
+ @Test
+ fun `Violation - domain depends on ui`() {
+ lint()
+ .files(
+ *LEGITIMATE_FILES,
+ TestFiles.kotlin(
+ """
+ package test.domain.interactor
+
+ import test.ui.viewmodel.ViewModel
+
+ class BadClass(
+ private val viewModel: ViewModel,
+ )
+ """.trimIndent()
+ )
+ )
+ .issues(
+ CleanArchitectureDependencyViolationDetector.ISSUE,
+ )
+ .testModes(TestMode.DEFAULT)
+ .run()
+ .expectWarningCount(1)
+ .expect(
+ expectedText =
+ """
+ src/test/domain/interactor/BadClass.kt:3: Warning: The domain layer may not depend on the ui layer. [CleanArchitectureDependencyViolation]
+ import test.ui.viewmodel.ViewModel
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """,
+ )
+ }
+
+ @Test
+ fun `Violation - ui depends on data`() {
+ lint()
+ .files(
+ *LEGITIMATE_FILES,
+ TestFiles.kotlin(
+ """
+ package test.ui.viewmodel
+
+ import test.data.repository.Repository
+
+ class BadClass(
+ private val repository: Repository,
+ )
+ """.trimIndent()
+ )
+ )
+ .issues(
+ CleanArchitectureDependencyViolationDetector.ISSUE,
+ )
+ .testModes(TestMode.DEFAULT)
+ .run()
+ .expectWarningCount(1)
+ .expect(
+ expectedText =
+ """
+ src/test/ui/viewmodel/BadClass.kt:3: Warning: The ui layer may not depend on the data layer. [CleanArchitectureDependencyViolation]
+ import test.data.repository.Repository
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """,
+ )
+ }
+
+ @Test
+ fun `Violation - shared depends on all other layers`() {
+ lint()
+ .files(
+ *LEGITIMATE_FILES,
+ TestFiles.kotlin(
+ """
+ package test.shared.model
+
+ import test.data.repository.Repository
+ import test.domain.interactor.Interactor
+ import test.ui.viewmodel.ViewModel
+
+ class BadClass(
+ private val repository: Repository,
+ private val interactor: Interactor,
+ private val viewmodel: ViewModel,
+ )
+ """.trimIndent()
+ )
+ )
+ .issues(
+ CleanArchitectureDependencyViolationDetector.ISSUE,
+ )
+ .testModes(TestMode.DEFAULT)
+ .run()
+ .expectWarningCount(3)
+ .expect(
+ expectedText =
+ """
+ src/test/shared/model/BadClass.kt:3: Warning: The shared layer may not depend on the data layer. [CleanArchitectureDependencyViolation]
+ import test.data.repository.Repository
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/shared/model/BadClass.kt:4: Warning: The shared layer may not depend on the domain layer. [CleanArchitectureDependencyViolation]
+ import test.domain.interactor.Interactor
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/shared/model/BadClass.kt:5: Warning: The shared layer may not depend on the ui layer. [CleanArchitectureDependencyViolation]
+ import test.ui.viewmodel.ViewModel
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 3 warnings
+ """,
+ )
+ }
+
+ @Test
+ fun `Violation - data depends on domain`() {
+ lint()
+ .files(
+ *LEGITIMATE_FILES,
+ TestFiles.kotlin(
+ """
+ package test.data.repository
+
+ import test.domain.interactor.Interactor
+
+ class BadClass(
+ private val interactor: Interactor,
+ )
+ """.trimIndent()
+ )
+ )
+ .issues(
+ CleanArchitectureDependencyViolationDetector.ISSUE,
+ )
+ .testModes(TestMode.DEFAULT)
+ .run()
+ .expectWarningCount(1)
+ .expect(
+ expectedText =
+ """
+ src/test/data/repository/BadClass.kt:3: Warning: The data layer may not depend on the domain layer. [CleanArchitectureDependencyViolation]
+ import test.domain.interactor.Interactor
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """,
+ )
+ }
+
+ companion object {
+ private val MODEL_FILE =
+ TestFiles.kotlin(
+ """
+ package test.shared.model
+
+ import test.some.other.thing.SomeOtherThing
+
+ data class Model(
+ private val name: String,
+ )
+ """.trimIndent()
+ )
+ private val REPOSITORY_FILE =
+ TestFiles.kotlin(
+ """
+ package test.data.repository
+
+ import test.shared.model.Model
+ import test.some.other.thing.SomeOtherThing
+
+ class Repository {
+ private val models = listOf(
+ Model("one"),
+ Model("two"),
+ Model("three"),
+ )
+
+ fun getModels(): List<Model> {
+ return models
+ }
+ }
+ """.trimIndent()
+ )
+ private val INTERACTOR_FILE =
+ TestFiles.kotlin(
+ """
+ package test.domain.interactor
+
+ import test.data.repository.Repository
+ import test.shared.model.Model
+
+ class Interactor(
+ private val repository: Repository,
+ ) {
+ fun getModels(): List<Model> {
+ return repository.getModels()
+ }
+ }
+ """.trimIndent()
+ )
+ private val VIEW_MODEL_FILE =
+ TestFiles.kotlin(
+ """
+ package test.ui.viewmodel
+
+ import test.domain.interactor.Interactor
+ import test.some.other.thing.SomeOtherThing
+
+ class ViewModel(
+ private val interactor: Interactor,
+ ) {
+ fun getNames(): List<String> {
+ return interactor.getModels().map { model -> model.name }
+ }
+ }
+ """.trimIndent()
+ )
+ private val NON_CLEAN_ARCHITECTURE_FILE =
+ TestFiles.kotlin(
+ """
+ package test.some.other.thing
+
+ import test.data.repository.Repository
+ import test.domain.interactor.Interactor
+ import test.ui.viewmodel.ViewModel
+
+ class SomeOtherThing {
+ init {
+ val viewModel = ViewModel(
+ interactor = Interactor(
+ repository = Repository(),
+ ),
+ )
+ }
+ }
+ """.trimIndent()
+ )
+ private val LEGITIMATE_FILES =
+ arrayOf(
+ MODEL_FILE,
+ REPOSITORY_FILE,
+ INTERACTOR_FILE,
+ VIEW_MODEL_FILE,
+ NON_CLEAN_ARCHITECTURE_FILE,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
index 259f0ed..cfc38df 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
@@ -33,14 +33,15 @@
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
import androidx.compose.material3.contentColorFor
+import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
@@ -64,11 +65,9 @@
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.findRootCoordinates
-import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewTreeLifecycleOwner
@@ -77,7 +76,6 @@
import com.android.systemui.animation.LaunchAnimator
import kotlin.math.max
import kotlin.math.min
-import kotlin.math.roundToInt
/**
* Create an expandable shape that can launch into an Activity or a Dialog.
@@ -170,25 +168,25 @@
val contentColor = controller.contentColor
val shape = controller.shape
- // TODO(b/230830644): Use movableContentOf to preserve the content state instead once the
- // Compose libraries have been updated and include aosp/2163631.
val wrappedContent =
- @Composable { controller: ExpandableController ->
- CompositionLocalProvider(
- LocalContentColor provides contentColor,
- ) {
- // We make sure that the content itself (wrapped by the background) is at least
- // 40.dp, which is the same as the M3 buttons. This applies even if onClick is
- // null, to make it easier to write expandables that are sometimes clickable and
- // sometimes not. There shouldn't be any Expandable smaller than 40dp because if
- // the expandable is not clickable directly, then something in its content should
- // be (and with a size >= 40dp).
- val minSize = 40.dp
- Box(
- Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize),
- contentAlignment = Alignment.Center,
+ remember(content) {
+ movableContentOf { expandable: Expandable ->
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
) {
- content(controller.expandable)
+ // We make sure that the content itself (wrapped by the background) is at least
+ // 40.dp, which is the same as the M3 buttons. This applies even if onClick is
+ // null, to make it easier to write expandables that are sometimes clickable and
+ // sometimes not. There shouldn't be any Expandable smaller than 40dp because if
+ // the expandable is not clickable directly, then something in its content
+ // should be (and with a size >= 40dp).
+ val minSize = 40.dp
+ Box(
+ Modifier.defaultMinSize(minWidth = minSize, minHeight = minSize),
+ contentAlignment = Alignment.Center,
+ ) {
+ content(expandable)
+ }
}
}
}
@@ -218,21 +216,8 @@
// If this expandable is expanded when it's being directly clicked on, let's ensure that it has
// the minimum interactive size followed by all M3 components (48.dp).
val minInteractiveSizeModifier =
- if (onClick != null && LocalMinimumTouchTargetEnforcement.current) {
- // TODO(b/242040009): Replace this by Modifier.minimumInteractiveComponentSize() once
- // http://aosp/2305511 is available.
- val minTouchSize = LocalViewConfiguration.current.minimumTouchTargetSize
- Modifier.layout { measurable, constraints ->
- // Copied from androidx.compose.material3.InteractiveComponentSize.kt
- val placeable = measurable.measure(constraints)
- val width = maxOf(placeable.width, minTouchSize.width.roundToPx())
- val height = maxOf(placeable.height, minTouchSize.height.roundToPx())
- layout(width, height) {
- val centerX = ((width - placeable.width) / 2f).roundToInt()
- val centerY = ((height - placeable.height) / 2f).roundToInt()
- placeable.place(centerX, centerY)
- }
- }
+ if (onClick != null) {
+ Modifier.minimumInteractiveComponentSize()
} else {
Modifier
}
@@ -270,7 +255,7 @@
.onGloballyPositioned {
controller.boundsInComposeViewRoot.value = it.boundsInRoot()
}
- ) { wrappedContent(controller) }
+ ) { wrappedContent(controller.expandable) }
}
else -> {
val clickModifier =
@@ -301,7 +286,7 @@
controller.boundsInComposeViewRoot.value = it.boundsInRoot()
},
) {
- wrappedContent(controller)
+ wrappedContent(controller.expandable)
}
}
}
@@ -315,7 +300,7 @@
animatorState: State<LaunchAnimator.State?>,
overlay: ViewGroupOverlay,
controller: ExpandableControllerImpl,
- content: @Composable (ExpandableController) -> Unit,
+ content: @Composable (Expandable) -> Unit,
composeViewRoot: View,
onOverlayComposeViewChanged: (View?) -> Unit,
density: Density,
@@ -370,7 +355,7 @@
// We center the content in the expanding container.
contentAlignment = Alignment.Center,
) {
- Box(contentModifier) { content(controller) }
+ Box(contentModifier) { content(controller.expandable) }
}
}
}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 6e728ce..e253fb9 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -17,13 +17,21 @@
package com.android.systemui.compose
+import android.content.Context
+import android.view.View
import androidx.activity.ComponentActivity
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
override fun isComposeAvailable(): Boolean = false
+ override fun composeInitializer(): ComposeInitializer {
+ throwComposeUnavailableError()
+ }
+
override fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
@@ -32,7 +40,15 @@
throwComposeUnavailableError()
}
- private fun throwComposeUnavailableError() {
+ override fun createFooterActionsView(
+ context: Context,
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner
+ ): View {
+ throwComposeUnavailableError()
+ }
+
+ private fun throwComposeUnavailableError(): Nothing {
error(
"Compose is not available. Make sure to check isComposeAvailable() before calling any" +
" other function on ComposeFacade."
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 6991ff8..1ea18fe 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -16,16 +16,24 @@
package com.android.systemui.compose
+import android.content.Context
+import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.compose.FooterActions
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
override fun isComposeAvailable(): Boolean = true
+ override fun composeInitializer(): ComposeInitializer = ComposeInitializerImpl
+
override fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
@@ -33,4 +41,14 @@
) {
activity.setContent { PlatformTheme { PeopleScreen(viewModel, onResult) } }
}
+
+ override fun createFooterActionsView(
+ context: Context,
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner,
+ ): View {
+ return ComposeView(context).apply {
+ setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } }
+ }
+ }
}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt
new file mode 100644
index 0000000..772c891
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.compose
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryController
+import androidx.savedstate.SavedStateRegistryOwner
+import com.android.compose.animation.ViewTreeSavedStateRegistryOwner
+import com.android.systemui.lifecycle.ViewLifecycleOwner
+
+internal object ComposeInitializerImpl : ComposeInitializer {
+ override fun onAttachedToWindow(root: View) {
+ if (ViewTreeLifecycleOwner.get(root) != null) {
+ error("root $root already has a LifecycleOwner")
+ }
+
+ val parent = root.parent
+ if (parent is View && parent.id != android.R.id.content) {
+ error(
+ "ComposeInitializer.onAttachedToWindow(View) must be called on the content child." +
+ "Outside of activities and dialogs, this is usually the top-most View of a " +
+ "window."
+ )
+ }
+
+ // The lifecycle owner, which is STARTED when [root] is visible and RESUMED when [root] is
+ // both visible and focused.
+ val lifecycleOwner = ViewLifecycleOwner(root)
+
+ // We create a trivial implementation of [SavedStateRegistryOwner] that does not do any save
+ // or restore because SystemUI process is always running and top-level windows using this
+ // initializer are created once, when the process is started.
+ val savedStateRegistryOwner =
+ object : SavedStateRegistryOwner {
+ private val savedStateRegistry =
+ SavedStateRegistryController.create(this).apply { performRestore(null) }
+
+ override fun getLifecycle(): Lifecycle = lifecycleOwner.lifecycle
+
+ override fun getSavedStateRegistry(): SavedStateRegistry {
+ return savedStateRegistry.savedStateRegistry
+ }
+ }
+
+ // We must call [ViewLifecycleOwner.onCreate] after creating the [SavedStateRegistryOwner]
+ // because `onCreate` might move the lifecycle state to STARTED which will make
+ // [SavedStateRegistryController.performRestore] throw.
+ lifecycleOwner.onCreate()
+
+ // Set the owners on the root. They will be reused by any ComposeView inside the root
+ // hierarchy.
+ ViewTreeLifecycleOwner.set(root, lifecycleOwner)
+ ViewTreeSavedStateRegistryOwner.set(root, savedStateRegistryOwner)
+ }
+
+ override fun onDetachedFromWindow(root: View) {
+ (ViewTreeLifecycleOwner.get(root) as ViewLifecycleOwner).onDestroy()
+ ViewTreeLifecycleOwner.set(root, null)
+ ViewTreeSavedStateRegistryOwner.set(root, null)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt
new file mode 100644
index 0000000..9eb78e1
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.compose.modifiers
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+
+/**
+ * Set a test tag on this node so that it is associated with [resId]. This node will then be
+ * accessible by integration tests using `sysuiResSelector(resId)`.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.sysuiResTag(resId: String): Modifier {
+ return this.semantics { testTagsAsResourceId = true }.testTag("com.android.systemui:id/$resId")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 23dacf9..3eeadae 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -51,6 +51,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.R
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
@@ -110,7 +111,9 @@
recentTiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
) {
- Column {
+ Column(
+ Modifier.sysuiResTag("top_level_with_conversations"),
+ ) {
Column(
Modifier.fillMaxWidth().padding(PeopleSpacePadding),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -132,7 +135,7 @@
}
LazyColumn(
- Modifier.fillMaxWidth(),
+ Modifier.fillMaxWidth().sysuiResTag("scroll_view"),
contentPadding =
PaddingValues(
top = 16.dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index 5c5ceef..349f5c3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -73,6 +73,7 @@
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
@@ -180,9 +181,9 @@
security?.let { SecurityButton(it, Modifier.weight(1f)) }
foregroundServices?.let { ForegroundServicesButton(it) }
- userSwitcher?.let { IconButton(it) }
- IconButton(viewModel.settings)
- viewModel.power?.let { IconButton(it) }
+ userSwitcher?.let { IconButton(it, Modifier.sysuiResTag("multi_user_switch")) }
+ IconButton(viewModel.settings, Modifier.sysuiResTag("settings_button_container"))
+ viewModel.power?.let { IconButton(it, Modifier.sysuiResTag("pm_lite")) }
}
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 462b90a..86bd5f2 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -54,7 +54,6 @@
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes) {
- var tag: String = "UnnamedClockView"
var logBuffer: LogBuffer? = null
private val time = Calendar.getInstance()
@@ -132,7 +131,7 @@
override fun onAttachedToWindow() {
super.onAttachedToWindow()
- logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
+ logBuffer?.log(TAG, DEBUG, "onAttachedToWindow")
refreshFormat()
}
@@ -148,7 +147,7 @@
time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
- logBuffer?.log(tag, DEBUG,
+ logBuffer?.log(TAG, DEBUG,
{ str1 = formattedText?.toString() },
{ "refreshTime: new formattedText=$str1" }
)
@@ -157,7 +156,7 @@
// relayout if the text didn't actually change.
if (!TextUtils.equals(text, formattedText)) {
text = formattedText
- logBuffer?.log(tag, DEBUG,
+ logBuffer?.log(TAG, DEBUG,
{ str1 = formattedText?.toString() },
{ "refreshTime: done setting new time text to: $str1" }
)
@@ -167,17 +166,17 @@
// without being notified TextInterpolator being notified.
if (layout != null) {
textAnimator?.updateLayout(layout)
- logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
+ logBuffer?.log(TAG, DEBUG, "refreshTime: done updating textAnimator layout")
}
requestLayout()
- logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
+ logBuffer?.log(TAG, DEBUG, "refreshTime: after requestLayout")
}
}
fun onTimeZoneChanged(timeZone: TimeZone?) {
time.timeZone = timeZone
refreshFormat()
- logBuffer?.log(tag, DEBUG,
+ logBuffer?.log(TAG, DEBUG,
{ str1 = timeZone?.toString() },
{ "onTimeZoneChanged newTimeZone=$str1" }
)
@@ -194,7 +193,7 @@
} else {
animator.updateLayout(layout)
}
- logBuffer?.log(tag, DEBUG, "onMeasure")
+ logBuffer?.log(TAG, DEBUG, "onMeasure")
}
override fun onDraw(canvas: Canvas) {
@@ -206,12 +205,12 @@
} else {
super.onDraw(canvas)
}
- logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
+ logBuffer?.log(TAG, DEBUG, "onDraw")
}
override fun invalidate() {
super.invalidate()
- logBuffer?.log(tag, DEBUG, "invalidate")
+ logBuffer?.log(TAG, DEBUG, "invalidate")
}
override fun onTextChanged(
@@ -221,7 +220,7 @@
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
- logBuffer?.log(tag, DEBUG,
+ logBuffer?.log(TAG, DEBUG,
{ str1 = text.toString() },
{ "onTextChanged text=$str1" }
)
@@ -238,7 +237,7 @@
}
fun animateColorChange() {
- logBuffer?.log(tag, DEBUG, "animateColorChange")
+ logBuffer?.log(TAG, DEBUG, "animateColorChange")
setTextStyle(
weight = lockScreenWeight,
textSize = -1f,
@@ -260,7 +259,7 @@
}
fun animateAppearOnLockscreen() {
- logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
+ logBuffer?.log(TAG, DEBUG, "animateAppearOnLockscreen")
setTextStyle(
weight = dozingWeight,
textSize = -1f,
@@ -285,7 +284,7 @@
if (isAnimationEnabled && textAnimator == null) {
return
}
- logBuffer?.log(tag, DEBUG, "animateFoldAppear")
+ logBuffer?.log(TAG, DEBUG, "animateFoldAppear")
setTextStyle(
weight = lockScreenWeightInternal,
textSize = -1f,
@@ -312,7 +311,7 @@
// Skip charge animation if dozing animation is already playing.
return
}
- logBuffer?.log(tag, DEBUG, "animateCharge")
+ logBuffer?.log(TAG, DEBUG, "animateCharge")
val startAnimPhase2 = Runnable {
setTextStyle(
weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -336,7 +335,7 @@
}
fun animateDoze(isDozing: Boolean, animate: Boolean) {
- logBuffer?.log(tag, DEBUG, "animateDoze")
+ logBuffer?.log(TAG, DEBUG, "animateDoze")
setTextStyle(
weight = if (isDozing) dozingWeight else lockScreenWeight,
textSize = -1f,
@@ -455,7 +454,7 @@
isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
else -> DOUBLE_LINE_FORMAT_12_HOUR
}
- logBuffer?.log(tag, DEBUG,
+ logBuffer?.log(TAG, DEBUG,
{ str1 = format?.toString() },
{ "refreshFormat format=$str1" }
)
@@ -466,6 +465,7 @@
fun dump(pw: PrintWriter) {
pw.println("$this")
+ pw.println(" alpha=$alpha")
pw.println(" measuredWidth=$measuredWidth")
pw.println(" measuredHeight=$measuredHeight")
pw.println(" singleLineInternal=$isSingleLineInternal")
@@ -626,7 +626,7 @@
}
companion object {
- private val TAG = AnimatableClockView::class.simpleName
+ private val TAG = AnimatableClockView::class.simpleName!!
const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index e138ef8..7645dec 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -88,13 +88,6 @@
events.onTimeTick()
}
- override fun setLogBuffer(logBuffer: LogBuffer) {
- smallClock.view.tag = "smallClockView"
- largeClock.view.tag = "largeClockView"
- smallClock.view.logBuffer = logBuffer
- largeClock.view.logBuffer = logBuffer
- }
-
open inner class DefaultClockFaceController(
override val view: AnimatableClockView,
) : ClockFaceController {
@@ -104,6 +97,12 @@
private var isRegionDark = false
protected var targetRegion: Rect? = null
+ override var logBuffer: LogBuffer?
+ get() = view.logBuffer
+ set(value) {
+ view.logBuffer = value
+ }
+
init {
view.setColors(currentColor, currentColor)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
index 5bb3707..cd9fb88 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
@@ -20,12 +20,15 @@
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
+import android.content.Intent
import android.database.ContentObserver
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
+import android.util.Log
import androidx.annotation.DrawableRes
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
+import java.net.URISyntaxException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -169,6 +172,8 @@
* If `null`, the button should not be shown.
*/
val enablementActionComponentName: String? = null,
+ /** Optional [Intent] to use to start an activity to configure this affordance. */
+ val configureIntent: Intent? = null,
)
/** Models a selection of a quick affordance on a slot. */
@@ -337,6 +342,11 @@
Contract.LockScreenQuickAffordances.AffordanceTable.Columns
.ENABLEMENT_COMPONENT_NAME
)
+ val configureIntentColumnIndex =
+ cursor.getColumnIndex(
+ Contract.LockScreenQuickAffordances.AffordanceTable.Columns
+ .CONFIGURE_INTENT
+ )
if (
idColumnIndex == -1 ||
nameColumnIndex == -1 ||
@@ -344,15 +354,17 @@
isEnabledColumnIndex == -1 ||
enablementInstructionsColumnIndex == -1 ||
enablementActionTextColumnIndex == -1 ||
- enablementComponentNameColumnIndex == -1
+ enablementComponentNameColumnIndex == -1 ||
+ configureIntentColumnIndex == -1
) {
return@buildList
}
while (cursor.moveToNext()) {
+ val affordanceId = cursor.getString(idColumnIndex)
add(
CustomizationProviderClient.Affordance(
- id = cursor.getString(idColumnIndex),
+ id = affordanceId,
name = cursor.getString(nameColumnIndex),
iconResourceId = cursor.getInt(iconColumnIndex),
isEnabled = cursor.getInt(isEnabledColumnIndex) == 1,
@@ -367,6 +379,10 @@
cursor.getString(enablementActionTextColumnIndex),
enablementActionComponentName =
cursor.getString(enablementComponentNameColumnIndex),
+ configureIntent =
+ cursor
+ .getString(configureIntentColumnIndex)
+ ?.toIntent(affordanceId = affordanceId),
)
)
}
@@ -504,7 +520,19 @@
.onStart { emit(Unit) }
}
+ private fun String.toIntent(
+ affordanceId: String,
+ ): Intent? {
+ return try {
+ Intent.parseUri(this, 0)
+ } catch (e: URISyntaxException) {
+ Log.w(TAG, "Cannot parse Uri into Intent for affordance with ID \"$affordanceId\"!")
+ null
+ }
+ }
+
companion object {
+ private const val TAG = "CustomizationProviderClient"
private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index 1e2e7d2..e4e9c46 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -113,6 +113,11 @@
* opens a destination where the user can re-enable the disabled affordance.
*/
const val ENABLEMENT_COMPONENT_NAME = "enablement_action_intent"
+ /**
+ * Byte array. Optional parcelled `Intent` to use to start an activity that can be
+ * used to configure the affordance.
+ */
+ const val CONFIGURE_INTENT = "configure_intent"
}
}
@@ -173,6 +178,9 @@
/** Flag denoting whether the customizable clocks feature is enabled. */
const val FLAG_NAME_CUSTOM_CLOCKS_ENABLED = "is_custom_clocks_feature_enabled"
+ /** Flag denoting whether the Wallpaper preview should use the full screen UI. */
+ const val FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW = "wallpaper_fullscreen_preview"
+
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
index 18e8a96..bf922bc 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt
@@ -21,4 +21,5 @@
const val MESSAGE_ID_SLOT_SELECTED = 1337
const val KEY_SLOT_ID = "slot_id"
const val KEY_INITIALLY_SELECTED_SLOT_ID = "initially_selected_slot_id"
+ const val KEY_HIGHLIGHT_QUICK_AFFORDANCES = "highlight_quick_affordances"
}
diff --git a/packages/SystemUI/docs/dagger.md b/packages/SystemUI/docs/dagger.md
index 8917013..9b4c21e 100644
--- a/packages/SystemUI/docs/dagger.md
+++ b/packages/SystemUI/docs/dagger.md
@@ -8,105 +8,110 @@
- [User's guide](https://google.github.io/dagger/users-guide)
-TODO: Add some links.
-
## State of the world
-Dagger 2 has been turned on for SystemUI and a early first pass has been taken
-for converting everything in [Dependency.java](packages/systemui/src/com/android/systemui/Dependency.java)
-to use Dagger. Since a lot of SystemUI depends on Dependency, stubs have been added to Dependency
-to proxy any gets through to the instances provided by dagger, this will allow migration of SystemUI
-through a number of CLs.
+Dagger 2 has been turned on for SystemUI and much of
+[Dependency.java](../src/com/android/systemui/Dependency.java)
+has been converted to use Dagger. Since a lot of SystemUI depends on Dependency,
+stubs have been added to Dependency to proxy any gets through to the instances
+provided by dagger, this will allow migration of SystemUI through a number of CLs.
### How it works in SystemUI
+There are three high level "scopes" of concern in SystemUI. They all represent
+singleton scopes, but serve different purposes.
+
+* `@Singleton` - Instances that are shared everywhere. There isn't a lot of
+ code in this scope. Things like the main thread, and Android Framework
+ provided instances mostly.
+* `@WMShell` - WindowManager related code in the SystemUI process. We don't
+ want this code relying on the rest of SystemUI, and we don't want the rest
+ of SystemUI peeking into its internals, so it runs in its own Subcomponent.
+* `@SysUISingleton` - Most of what would be considered "SystemUI". Most feature
+ work by SystemUI developers goes into this scope. Useful interfaces from
+ WindowManager are made available inside this Subcomponent.
+
+The root dagger graph is created by an instance of `SystemUIInitializer`.
+See [README.md](../README.md) for more details.
For the classes that we're using in Dependency and are switching to dagger, the
equivalent dagger version is using `@Singleton` and therefore only has one instance.
To have the single instance span all of SystemUI and be easily accessible for
other components, there is a single root `@Component` that exists that generates
-these. The component lives in [SystemUIFactory](packages/systemui/src/com/android/systemui/SystemUIFactory.java)
-and is called `SystemUIRootComponent`.
-
-```java
-
-@Singleton
-@Component(modules = {SystemUIFactory.class, DependencyProvider.class, DependencyBinder.class,
- ContextHolder.class})
-public interface SystemUIRootComponent {
- @Singleton
- Dependency.DependencyInjector createDependency();
-}
-```
-
-The root component is composed of root modules, which in turn provide the global singleton
-dependencies across all of SystemUI.
-
-- `SystemUIFactory` `@Provides` dependencies that need to be overridden by SystemUI
-variants (like other form factors e.g. Car).
-
-- `DependencyBinder` creates the mapping from interfaces to implementation classes.
-
-- `DependencyProvider` provides or binds any remaining depedencies required.
-
-### Adding injection to a new SystemUI object
-
-SystemUI object are made injectable by adding an entry in `SystemUIBinder`. SystemUIApplication uses
-information in that file to locate and construct an instance of the requested SystemUI class.
+these. The component lives in
+[ReferenceGlobalRootComponent.java](../src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java).
### Adding a new injectable object
-First tag the constructor with `@Inject`. Also tag it with `@Singleton` if only one
-instance should be created.
+First annotate the constructor with `@Inject`. Also annotate it with
+`@SysUISingleton` if only one instance should be created.
-```java
-@Singleton
-public class SomethingController {
- @Inject
- public SomethingController(Context context,
- @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
- // context and mainHandler will be automatically populated.
- }
+```kotlin
+@SysUISingleton
+class FeatureStartable
+@Inject
+constructor(
+/* ... */
+) {
+ // ...
}
```
-If you have an interface class and an implementation class, dagger needs to know
-how to map it. The simplest way to do this is to add an `@Provides` method to
-DependencyProvider. The type of the return value tells dagger which dependency it's providing.
+If you have an interface class and an implementation class, Dagger needs to
+know how to map it. The simplest way to do this is to add an `@Binds` method
+in a module. The type of the return value tells dagger which dependency it's
+providing:
-```java
-public class DependencyProvider {
- //...
- @Singleton
- @Provides
- public SomethingController provideSomethingController(Context context,
- @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
- return new SomethingControllerImpl(context, mainHandler);
- }
+```kotlin
+@Module
+abstract class FeatureModule {
+ @Binds
+ abstract fun bindsFeature(impl: FeatureImpl): Feature
}
```
-If you need to access this from Dependency#get, then add an adapter to Dependency
-that maps to the instance provided by Dagger. The changes should be similar
-to the following diff.
+If you have a class that you want to make injectable that has can not
+be easily constructed by Dagger, write a `@Provides` method for it:
-```java
-public class Dependency {
- //...
- @Inject Lazy<SomethingController> mSomethingController;
- //...
- public void start() {
- //...
- mProviders.put(SomethingController.class, mSomethingController::get);
- }
+```kotlin
+@Module
+abstract class FeatureModule {
+ @Module
+ companion object {
+ @Provides
+ fun providesFeature(ctx: Context): Feature {
+ return FeatureImpl.constructFromContext(ctx)
+ }
+ }
}
```
+### Module Organization
+
+Please define your modules on _at least_ per-package level. If the scope of a
+package grows to encompass a great number of features, create per-feature
+modules.
+
+**Do not create catch-all modules.** Those quickly grow unwieldy and
+unmaintainable. Any that exist today should be refactored into obsolescence.
+
+You can then include your module in one of three places:
+
+1) Within another module that depends on it. Ideally, this creates a clean
+ dependency graph between features and utilities.
+2) For features that should exist in all versions of SystemUI (AOSP and
+ any variants), include the module in
+ [SystemUIModule.java](../src/com/android/systemui/dagger/SystemUIModule.java).
+3) For features that should exist only in AOSP, include the module in
+ [ReferenceSystemUIModule.java](../src/com/android/systemui/dagger/ReferenceSystemUIModule.java).
+ Similarly, if you are working on a custom version of SystemUI and have code
+ specific to your version, include it in a module specific to your version.
+
### Using injection with Fragments
Fragments are created as part of the FragmentManager, so they need to be
setup so the manager knows how to create them. To do that, add a method
to com.android.systemui.fragments.FragmentService$FragmentCreator that
-returns your fragment class. Thats all thats required, once the method
+returns your fragment class. That is all that is required, once the method
exists, FragmentService will automatically pick it up and use injection
whenever your fragment needs to be created.
@@ -123,48 +128,11 @@
FragmentHostManager.get(view).create(NavigationBarFragment.class);
```
-### Using injection with Views
-
-DO NOT ADD NEW VIEW INJECTION. VIEW INJECTION IS BEING ACTIVELY DEPRECATED.
-
-Needing to inject objects into your View's constructor generally implies you
-are doing more work in your presentation layer than is advisable.
-Instead, create an injected controller for you view, inject into the
-controller, and then attach the view to the controller after inflation.
-
-View injection generally causes headaches while testing, as inflating a view
-(which may in turn inflate other views) implicitly causes a Dagger graph to
-be stood up, which may or may not contain the appropriately
-faked/mocked/stubbed objects. It is a hard to control process.
-
## Updating Dagger2
We depend on the Dagger source found in external/dagger2. We should automatically pick up on updates
when that repository is updated.
-
-*Deprecated:*
-
-Binaries can be downloaded from https://repo1.maven.org/maven2/com/google/dagger/ and then loaded
-into
-[/prebuilts/tools/common/m2/repository/com/google/dagger/](http://cs/android/prebuilts/tools/common/m2/repository/com/google/dagger/)
-
-The following commands should work, substituting in the version that you are looking for:
-
-````
-cd prebuilts/tools/common/m2/repository/com/google/dagger/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger/2.28.1/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.28.1/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-spi/2.28.1/
-
-wget -r -np -nH --cut-dirs=4 -erobots=off -R "index.html*" -U "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" https://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.28.1/
-````
-
-Then update `prebuilts/tools/common/m2/Android.bp` to point at your new jars.
## TODO List
- - Eliminate usages of Dependency#get
- - Add links in above TODO
+ - Eliminate usages of Dependency#get: http://b/hotlists/3940788
diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md
index ccb35fa..79d5718 100644
--- a/packages/SystemUI/docs/device-entry/quickaffordance.md
+++ b/packages/SystemUI/docs/device-entry/quickaffordance.md
@@ -52,6 +52,10 @@
* Unselect an already-selected quick affordance from a slot
* Unselect all already-selected quick affordances from a slot
+## Testing
+* Add a unit test for your implementation of `KeyguardQuickAffordanceConfig`
+* Manually verify that your implementation works in multi-user environments from both the main user and a secondary user
+
## Debugging
To see the current state of the system, you can run `dumpsys`:
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index f53e3f6..1a67691e 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -1,8 +1,5 @@
+packages/SystemUI
--packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
--packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
-packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
--packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
-packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
@@ -200,13 +197,10 @@
-packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
--packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
-packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt
-packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
-packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -322,8 +316,8 @@
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
-packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
--packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
--packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
+-packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
-packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
-packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
-packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -614,7 +608,6 @@
-packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index dee0f5c..314c736 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -80,6 +80,7 @@
internal class HueVibrantSecondary() : Hue {
val hueToRotations = listOf(Pair(0, 18), Pair(41, 15), Pair(61, 10), Pair(101, 12),
Pair(131, 15), Pair(181, 18), Pair(251, 15), Pair(301, 12), Pair(360, 12))
+
override fun get(sourceColor: Cam): Double {
return getHueRotation(sourceColor.hue, hueToRotations)
}
@@ -88,6 +89,7 @@
internal class HueVibrantTertiary() : Hue {
val hueToRotations = listOf(Pair(0, 35), Pair(41, 30), Pair(61, 20), Pair(101, 25),
Pair(131, 30), Pair(181, 35), Pair(251, 30), Pair(301, 25), Pair(360, 25))
+
override fun get(sourceColor: Cam): Double {
return getHueRotation(sourceColor.hue, hueToRotations)
}
@@ -96,6 +98,7 @@
internal class HueExpressiveSecondary() : Hue {
val hueToRotations = listOf(Pair(0, 45), Pair(21, 95), Pair(51, 45), Pair(121, 20),
Pair(151, 45), Pair(191, 90), Pair(271, 45), Pair(321, 45), Pair(360, 45))
+
override fun get(sourceColor: Cam): Double {
return getHueRotation(sourceColor.hue, hueToRotations)
}
@@ -104,6 +107,7 @@
internal class HueExpressiveTertiary() : Hue {
val hueToRotations = listOf(Pair(0, 120), Pair(21, 120), Pair(51, 20), Pair(121, 45),
Pair(151, 20), Pair(191, 15), Pair(271, 20), Pair(321, 120), Pair(360, 120))
+
override fun get(sourceColor: Cam): Double {
return getHueRotation(sourceColor.hue, hueToRotations)
}
@@ -148,11 +152,11 @@
}
internal class CoreSpec(
- val a1: TonalSpec,
- val a2: TonalSpec,
- val a3: TonalSpec,
- val n1: TonalSpec,
- val n2: TonalSpec
+ val a1: TonalSpec,
+ val a2: TonalSpec,
+ val a3: TonalSpec,
+ val n1: TonalSpec,
+ val n2: TonalSpec
)
enum class Style(internal val coreSpec: CoreSpec) {
@@ -214,51 +218,86 @@
)),
}
+class TonalPalette {
+ val shadeKeys = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
+ val allShades: List<Int>
+ val allShadesMapped: Map<Int, Int>
+ val baseColor: Int
+
+ internal constructor(spec: TonalSpec, seedColor: Int) {
+ val seedCam = Cam.fromInt(seedColor)
+ allShades = spec.shades(seedCam)
+ allShadesMapped = shadeKeys.zip(allShades).toMap()
+
+ val h = spec.hue.get(seedCam).toFloat()
+ val c = spec.chroma.get(seedCam).toFloat()
+ baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor))
+ }
+
+ val s10: Int get() = this.allShades[0]
+ val s50: Int get() = this.allShades[1]
+ val s100: Int get() = this.allShades[2]
+ val s200: Int get() = this.allShades[3]
+ val s300: Int get() = this.allShades[4]
+ val s400: Int get() = this.allShades[5]
+ val s500: Int get() = this.allShades[6]
+ val s600: Int get() = this.allShades[7]
+ val s700: Int get() = this.allShades[8]
+ val s800: Int get() = this.allShades[9]
+ val s900: Int get() = this.allShades[10]
+ val s1000: Int get() = this.allShades[11]
+}
+
class ColorScheme(
- @ColorInt val seed: Int,
- val darkTheme: Boolean,
- val style: Style = Style.TONAL_SPOT
+ @ColorInt val seed: Int,
+ val darkTheme: Boolean,
+ val style: Style = Style.TONAL_SPOT
) {
- val accent1: List<Int>
- val accent2: List<Int>
- val accent3: List<Int>
- val neutral1: List<Int>
- val neutral2: List<Int>
+ val accent1: TonalPalette
+ val accent2: TonalPalette
+ val accent3: TonalPalette
+ val neutral1: TonalPalette
+ val neutral2: TonalPalette
constructor(@ColorInt seed: Int, darkTheme: Boolean) :
this(seed, darkTheme, Style.TONAL_SPOT)
@JvmOverloads
constructor(
- wallpaperColors: WallpaperColors,
- darkTheme: Boolean,
- style: Style = Style.TONAL_SPOT
+ wallpaperColors: WallpaperColors,
+ darkTheme: Boolean,
+ style: Style = Style.TONAL_SPOT
) :
this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
+ val allHues: List<TonalPalette>
+ get() {
+ return listOf(accent1, accent2, accent3, neutral1, neutral2)
+ }
+
val allAccentColors: List<Int>
get() {
val allColors = mutableListOf<Int>()
- allColors.addAll(accent1)
- allColors.addAll(accent2)
- allColors.addAll(accent3)
+ allColors.addAll(accent1.allShades)
+ allColors.addAll(accent2.allShades)
+ allColors.addAll(accent3.allShades)
return allColors
}
val allNeutralColors: List<Int>
get() {
val allColors = mutableListOf<Int>()
- allColors.addAll(neutral1)
- allColors.addAll(neutral2)
+ allColors.addAll(neutral1.allShades)
+ allColors.addAll(neutral2.allShades)
return allColors
}
val backgroundColor
- get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1[8] else neutral1[0], 0xFF)
+ get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1.s700 else neutral1.s10, 0xFF)
val accentColor
- get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1[2] else accent1[6], 0xFF)
+ get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1.s100 else accent1.s500, 0xFF)
init {
val proposedSeedCam = Cam.fromInt(seed)
@@ -269,24 +308,26 @@
} else {
seed
}
- val camSeed = Cam.fromInt(seedArgb)
- accent1 = style.coreSpec.a1.shades(camSeed)
- accent2 = style.coreSpec.a2.shades(camSeed)
- accent3 = style.coreSpec.a3.shades(camSeed)
- neutral1 = style.coreSpec.n1.shades(camSeed)
- neutral2 = style.coreSpec.n2.shades(camSeed)
+
+ accent1 = TonalPalette(style.coreSpec.a1, seedArgb)
+ accent2 = TonalPalette(style.coreSpec.a2, seedArgb)
+ accent3 = TonalPalette(style.coreSpec.a3, seedArgb)
+ neutral1 = TonalPalette(style.coreSpec.n1, seedArgb)
+ neutral2 = TonalPalette(style.coreSpec.n2, seedArgb)
}
+ val shadeCount get() = this.accent1.allShades.size
+
override fun toString(): String {
return "ColorScheme {\n" +
" seed color: ${stringForColor(seed)}\n" +
" style: $style\n" +
" palettes: \n" +
- " ${humanReadable("PRIMARY", accent1)}\n" +
- " ${humanReadable("SECONDARY", accent2)}\n" +
- " ${humanReadable("TERTIARY", accent3)}\n" +
- " ${humanReadable("NEUTRAL", neutral1)}\n" +
- " ${humanReadable("NEUTRAL VARIANT", neutral2)}\n" +
+ " ${humanReadable("PRIMARY", accent1.allShades)}\n" +
+ " ${humanReadable("SECONDARY", accent2.allShades)}\n" +
+ " ${humanReadable("TERTIARY", accent3.allShades)}\n" +
+ " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" +
+ " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" +
"}"
}
@@ -385,7 +426,8 @@
val existingSeedNearby = seeds.find {
val hueA = intToCam[int]!!.hue
val hueB = intToCam[it]!!.hue
- hueDiff(hueA, hueB) < i } != null
+ hueDiff(hueA, hueB) < i
+ } != null
if (existingSeedNearby) {
continue
}
@@ -460,9 +502,9 @@
}
private fun huePopulations(
- camByColor: Map<Int, Cam>,
- populationByColor: Map<Int, Double>,
- filter: Boolean = true
+ camByColor: Map<Int, Cam>,
+ populationByColor: Map<Int, Double>,
+ filter: Boolean = true
): List<Double> {
val huePopulation = List(size = 360, init = { 0.0 }).toMutableList()
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 7709f21..fb1c454 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -29,6 +29,7 @@
"src/**/*.java",
"src/**/*.kt",
"bcsmartspace/src/**/*.java",
+ "bcsmartspace/src/**/*.kt",
],
static_libs: [
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
new file mode 100644
index 0000000..509f022
--- /dev/null
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.plugins
+
+// TODO(b/265360975): Evaluate this plugin approach.
+/** Plugin to provide BC smartspace configuration */
+interface BcSmartspaceConfigPlugin {
+ /** Gets default date/weather disabled status. */
+ val isDefaultDateWeatherDisabled: Boolean
+}
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
index 51f5baa..e0cc8f4 100644
--- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
@@ -50,16 +50,24 @@
String TAG = "BcSmartspaceDataPlugin";
/** Register a listener to get Smartspace data. */
- void registerListener(SmartspaceTargetListener listener);
+ default void registerListener(SmartspaceTargetListener listener) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/** Unregister a listener. */
- void unregisterListener(SmartspaceTargetListener listener);
+ default void unregisterListener(SmartspaceTargetListener listener) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/** Register a SmartspaceEventNotifier. */
- default void registerSmartspaceEventNotifier(SmartspaceEventNotifier notifier) {}
+ default void registerSmartspaceEventNotifier(SmartspaceEventNotifier notifier) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/** Push a SmartspaceTargetEvent to the SmartspaceEventNotifier. */
- default void notifySmartspaceEvent(SmartspaceTargetEvent event) {}
+ default void notifySmartspaceEvent(SmartspaceTargetEvent event) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/** Allows for notifying the SmartspaceSession of SmartspaceTargetEvents. */
interface SmartspaceEventNotifier {
@@ -72,16 +80,20 @@
* will be responsible for correctly setting the LayoutParams
*/
default SmartspaceView getView(ViewGroup parent) {
- return null;
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
}
/**
* As the smartspace view becomes available, allow listeners to receive an event.
*/
- default void addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) { }
+ default void addOnAttachStateChangeListener(View.OnAttachStateChangeListener listener) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/** Updates Smartspace data and propagates it to any listeners. */
- void onTargetsAvailable(List<SmartspaceTarget> targets);
+ default void onTargetsAvailable(List<SmartspaceTarget> targets) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/** Provides Smartspace data to registered listeners. */
interface SmartspaceTargetListener {
@@ -94,6 +106,13 @@
void registerDataProvider(BcSmartspaceDataPlugin plugin);
/**
+ * Sets {@link BcSmartspaceConfigPlugin}.
+ */
+ default void registerConfigProvider(BcSmartspaceConfigPlugin configProvider) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
+
+ /**
* Primary color for unprotected text
*/
void setPrimaryTextColor(int color);
@@ -133,28 +152,38 @@
/**
* Set or clear Do Not Disturb information.
*/
- void setDnd(@Nullable Drawable image, @Nullable String description);
+ default void setDnd(@Nullable Drawable image, @Nullable String description) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/**
* Set or clear next alarm information
*/
- void setNextAlarm(@Nullable Drawable image, @Nullable String description);
+ default void setNextAlarm(@Nullable Drawable image, @Nullable String description) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/**
* Set or clear device media playing
*/
- void setMediaTarget(@Nullable SmartspaceTarget target);
+ default void setMediaTarget(@Nullable SmartspaceTarget target) {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/**
* Get the index of the currently selected page.
*/
- int getSelectedPage();
+ default int getSelectedPage() {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
/**
* Return the top padding value from the currently visible card, or 0 if there is no current
* card.
*/
- int getCurrentCardTopPadding();
+ default int getCurrentCardTopPadding() {
+ throw new UnsupportedOperationException("Not implemented by " + getClass());
+ }
}
/** Interface for launching Intents, which can differ on the lockscreen */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 66e44b9..a2a0709 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -71,9 +71,6 @@
/** Optional method for dumping debug information */
fun dump(pw: PrintWriter) {}
-
- /** Optional method for debug logging */
- fun setLogBuffer(logBuffer: LogBuffer) {}
}
/** Interface for a specific clock face version rendered by the clock */
@@ -83,6 +80,9 @@
/** Events specific to this clock face */
val events: ClockFaceEvents
+
+ /** Some clocks may log debug information */
+ var logBuffer: LogBuffer?
}
/** Events that should call when various rendering parameters change */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
index 6436dcb..e99b214 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -159,8 +159,13 @@
* bug report more actionable, so using the [log] with a messagePrinter to add more detail to
* every log may do more to improve overall logging than adding more logs with this method.
*/
- fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
- log(tag, level, { str1 = message }, { str1!! })
+ @JvmOverloads
+ fun log(
+ tag: String,
+ level: LogLevel,
+ @CompileTimeConstant message: String,
+ exception: Throwable? = null,
+ ) = log(tag, level, { str1 = message }, { str1!! }, exception)
/**
* You should call [log] instead of this method.
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
index d3fabac..faf1b78 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -21,6 +21,7 @@
import android.net.Uri
import android.os.Handler
import android.os.Looper
+import android.os.Trace
import android.provider.Settings
/**
@@ -51,14 +52,21 @@
}
}
+ private fun clearCache() {
+ Trace.beginSection("LogcatEchoTrackerDebug#clearCache")
+ cachedBufferLevels.clear()
+ Trace.endSection()
+ }
+
private fun attach(mainLooper: Looper) {
+ Trace.beginSection("LogcatEchoTrackerDebug#attach")
contentResolver.registerContentObserver(
Settings.Global.getUriFor(BUFFER_PATH),
true,
object : ContentObserver(Handler(mainLooper)) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
- cachedBufferLevels.clear()
+ clearCache()
}
}
)
@@ -69,10 +77,11 @@
object : ContentObserver(Handler(mainLooper)) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
- cachedTagLevels.clear()
+ clearCache()
}
}
)
+ Trace.endSection()
}
/** Whether [bufferName] should echo messages of [level] or higher to logcat. */
@@ -97,9 +106,12 @@
private fun readSetting(path: String): LogLevel {
return try {
+ Trace.beginSection("LogcatEchoTrackerDebug#readSetting")
parseProp(Settings.Global.getString(contentResolver, path))
} catch (_: Settings.SettingNotFoundException) {
DEFAULT_LEVEL
+ } finally {
+ Trace.endSection()
}
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
index 9ed3bac..70b5d73 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
@@ -105,6 +105,11 @@
default void onDozingChanged(boolean isDozing) {}
/**
+ * Callback to be notified when Dreaming changes. Dreaming is stored separately from state.
+ */
+ default void onDreamingChanged(boolean isDreaming) {}
+
+ /**
* Callback to be notified when the doze amount changes. Useful for animations.
* Note: this will be called for each animation frame. Please be careful to avoid
* performance regressions.
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index f96644f..5fc9193 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -16,6 +16,50 @@
public <init>();
}
+# Needed to ensure callback field references are kept in their respective
+# owning classes when the downstream callback registrars only store weak refs.
+# TODO(b/264686688): Handle these cases with more targeted annotations.
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ private com.android.keyguard.KeyguardUpdateMonitorCallback *;
+ private com.android.systemui.privacy.PrivacyConfig$Callback *;
+ private com.android.systemui.privacy.PrivacyItemController$Callback *;
+ private com.android.systemui.settings.UserTracker$Callback *;
+ private com.android.systemui.statusbar.phone.StatusBarWindowCallback *;
+ private com.android.systemui.util.service.Observer$Callback *;
+ private com.android.systemui.util.service.ObservableServiceConnection$Callback *;
+}
+# Note that these rules are temporary companions to the above rules, required
+# for cases like Kotlin where fields with anonymous types use the anonymous type
+# rather than the supertype.
+-if class * extends com.android.keyguard.KeyguardUpdateMonitorCallback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+-if class * extends com.android.systemui.privacy.PrivacyConfig$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+-if class * extends com.android.systemui.privacy.PrivacyItemController$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+-if class * extends com.android.systemui.settings.UserTracker$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+-if class * extends com.android.systemui.statusbar.phone.StatusBarWindowCallback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+-if class * extends com.android.systemui.util.service.Observer$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+-if class * extends com.android.systemui.util.service.ObservableServiceConnection$Callback
+-keepclassmembers,allowaccessmodification class com.android.systemui.**, com.android.keyguard.** {
+ <1> *;
+}
+
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
index 2cac9c7..90851e2 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
@@ -45,7 +45,7 @@
android:id="@+id/user_switcher_header"
android:textDirection="locale"
android:layout_width="@dimen/bouncer_user_switcher_width"
- android:layout_height="wrap_content" />
+ android:layout_height="match_parent" />
</com.android.keyguard.KeyguardUserSwitcherAnchor>
</LinearLayout>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
index 2b7bdc2..c772c96 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -27,7 +27,7 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
- androidprv:layout_maxWidth="@dimen/keyguard_security_width"
+ androidprv:layout_maxWidth="@dimen/biometric_auth_pattern_view_max_size"
android:layout_gravity="center_horizontal|bottom"
android:clipChildren="false"
android:clipToPadding="false">
diff --git a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml b/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml
deleted file mode 100644
index a3c37e4..0000000
--- a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/any/dimens.xml
-**
-** Copyright 2013, 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.
-*/
--->
-<resources>
- <!-- Height of the sliding KeyguardSecurityContainer
- (includes 2x keyguard_security_view_top_margin) -->
- <dimen name="keyguard_security_height">550dp</dimen>
-</resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index a1068c6..6c8db91 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -25,9 +25,6 @@
<!-- Margin around the various security views -->
<dimen name="keyguard_security_view_top_margin">12dp</dimen>
- <!-- Padding for the lock icon on the keyguard -->
- <dimen name="lock_icon_padding">16dp</dimen>
-
<!-- Overload default clock widget parameters -->
<dimen name="widget_big_font_size">100dp</dimen>
<dimen name="widget_label_font_size">18sp</dimen>
diff --git a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml
index 1dc61c5..b7a1bb4 100644
--- a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml
@@ -17,10 +17,5 @@
*/
-->
<resources>
-
- <!-- Height of the sliding KeyguardSecurityContainer
- (includes 2x keyguard_security_view_top_margin) -->
- <dimen name="keyguard_security_height">470dp</dimen>
-
<dimen name="widget_big_font_size">100dp</dimen>
</resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index c5ffdc0..6cc5b9d 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -29,9 +29,6 @@
(includes 2x keyguard_security_view_top_margin) -->
<dimen name="keyguard_security_height">420dp</dimen>
- <!-- Max Height of the sliding KeyguardSecurityContainer
- (includes 2x keyguard_security_view_top_margin) -->
-
<!-- pin/password field max height -->
<dimen name="keyguard_password_height">80dp</dimen>
diff --git a/packages/SystemUI/res/drawable/ic_camera.xml b/packages/SystemUI/res/drawable/ic_camera.xml
new file mode 100644
index 0000000..ef1406c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_camera.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M7,42Q5.8,42 4.9,41.1Q4,40.2 4,39V13.35Q4,12.15 4.9,11.25Q5.8,10.35 7,10.35H14.35L18,6H30L33.65,10.35H41Q42.2,10.35 43.1,11.25Q44,12.15 44,13.35V39Q44,40.2 43.1,41.1Q42.2,42 41,42ZM7,39H41Q41,39 41,39Q41,39 41,39V13.35Q41,13.35 41,13.35Q41,13.35 41,13.35H7Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM7,39Q7,39 7,39Q7,39 7,39V13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35Q7,13.35 7,13.35V39Q7,39 7,39Q7,39 7,39ZM24,34.7Q27.5,34.7 30,32.225Q32.5,29.75 32.5,26.2Q32.5,22.7 30,20.2Q27.5,17.7 24,17.7Q20.45,17.7 17.975,20.2Q15.5,22.7 15.5,26.2Q15.5,29.75 17.975,32.225Q20.45,34.7 24,34.7ZM24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Q24,26.2 24,26.2Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml
new file mode 100644
index 0000000..08c5aaf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="13dp"
+ android:height="13dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_shortcut_keyguard.xml
similarity index 92%
copy from packages/SystemUI/res/drawable/ic_note_task_button.xml
copy to packages/SystemUI/res/drawable/ic_note_task_shortcut_keyguard.xml
index bb5e224..ee8d4883 100644
--- a/packages/SystemUI/res/drawable/ic_note_task_button.xml
+++ b/packages/SystemUI/res/drawable/ic_note_task_shortcut_keyguard.xml
@@ -19,10 +19,13 @@
android:viewportHeight="24"
android:viewportWidth="24">
<path
- android:fillColor="#636C6F"
+ android:fillAlpha="1"
+ android:fillColor="@android:color/white"
+ android:fillType="nonZero"
android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" />
<path
- android:fillColor="#636C6F"
- android:fillType="evenOdd"
+ android:fillAlpha="1"
+ android:fillColor="@android:color/white"
+ android:fillType="nonZero"
android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" />
</vector>
diff --git a/packages/SystemUI/res/drawable/ic_note_task_button.xml b/packages/SystemUI/res/drawable/ic_note_task_shortcut_widget.xml
similarity index 95%
rename from packages/SystemUI/res/drawable/ic_note_task_button.xml
rename to packages/SystemUI/res/drawable/ic_note_task_shortcut_widget.xml
index bb5e224..7590182 100644
--- a/packages/SystemUI/res/drawable/ic_note_task_button.xml
+++ b/packages/SystemUI/res/drawable/ic_note_task_shortcut_widget.xml
@@ -19,10 +19,13 @@
android:viewportHeight="24"
android:viewportWidth="24">
<path
+ android:fillAlpha="1"
android:fillColor="#636C6F"
+ android:fillType="nonZero"
android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z" />
<path
+ android:fillAlpha="1"
android:fillColor="#636C6F"
- android:fillType="evenOdd"
+ android:fillType="nonZero"
android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z" />
</vector>
diff --git a/packages/SystemUI/res/drawable/ic_videocam.xml b/packages/SystemUI/res/drawable/ic_videocam.xml
new file mode 100644
index 0000000..de2bc7b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_videocam.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M7,40Q5.8,40 4.9,39.1Q4,38.2 4,37V11Q4,9.8 4.9,8.9Q5.8,8 7,8H33Q34.2,8 35.1,8.9Q36,9.8 36,11V21.75L44,13.75V34.25L36,26.25V37Q36,38.2 35.1,39.1Q34.2,40 33,40ZM7,37H33Q33,37 33,37Q33,37 33,37V11Q33,11 33,11Q33,11 33,11H7Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37ZM7,37Q7,37 7,37Q7,37 7,37V11Q7,11 7,11Q7,11 7,11Q7,11 7,11Q7,11 7,11V37Q7,37 7,37Q7,37 7,37Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
index 18fcebb..87b5a4c 100644
--- a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml
@@ -16,53 +16,13 @@
* limitations under the License.
*/
-->
-<selector
+<shape
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-
- <item android:state_selected="true">
- <layer-list>
- <item
- android:left="3dp"
- android:top="3dp"
- android:right="3dp"
- android:bottom="3dp">
- <shape android:shape="oval">
- <solid android:color="?androidprv:attr/colorSurface"/>
- <size
- android:width="@dimen/keyguard_affordance_width"
- android:height="@dimen/keyguard_affordance_height"/>
- </shape>
- </item>
-
- <item>
- <shape android:shape="oval">
- <stroke
- android:color="@color/control_primary_text"
- android:width="2dp"/>
- <size
- android:width="@dimen/keyguard_affordance_width"
- android:height="@dimen/keyguard_affordance_height"/>
- </shape>
- </item>
- </layer-list>
- </item>
-
- <item>
- <layer-list>
- <item
- android:left="3dp"
- android:top="3dp"
- android:right="3dp"
- android:bottom="3dp">
- <shape android:shape="oval">
- <solid android:color="?androidprv:attr/colorSurface"/>
- <size
- android:width="@dimen/keyguard_affordance_width"
- android:height="@dimen/keyguard_affordance_height"/>
- </shape>
- </item>
- </layer-list>
- </item>
-
-</selector>
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorSurface"/>
+ <size
+ android:width="@dimen/keyguard_affordance_fixed_width"
+ android:height="@dimen/keyguard_affordance_fixed_height"/>
+ <corners android:radius="@dimen/keyguard_affordance_fixed_radius" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_selected_border.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_selected_border.xml
new file mode 100644
index 0000000..7d03b0d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_selected_border.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 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.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_selected="true">
+ <shape android:shape="oval">
+ <stroke
+ android:color="?android:attr/textColorPrimary"
+ android:width="2dp"/>
+ <size
+ android:width="@dimen/keyguard_affordance_fixed_width"
+ android:height="@dimen/keyguard_affordance_fixed_height"/>
+ </shape>
+ </item>
+</selector>
diff --git a/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
new file mode 100644
index 0000000..3807b92
--- /dev/null
+++ b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ ~
+ -->
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/white"/>
+ <corners android:radius="28dp" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorSurface" />
+ <corners android:radius="28dp" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml
index 857632e..53122c1 100644
--- a/packages/SystemUI/res/drawable/overlay_badge_background.xml
+++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2020 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.
@@ -14,8 +14,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:shape="oval">
- <solid android:color="?androidprv:attr/colorSurface"/>
-</shape>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M0,0M48,48"/>
+</vector>
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index a3dd334..3505a3e 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -71,8 +71,8 @@
<com.android.internal.widget.LockPatternView
android:id="@+id/lockPattern"
android:layout_gravity="center"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_width="@dimen/biometric_auth_pattern_view_size"
+ android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
<TextView
android:id="@+id/error"
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index 4af9970..147ea82 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -67,8 +67,8 @@
<com.android.internal.widget.LockPatternView
android:id="@+id/lockPattern"
android:layout_gravity="center"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_width="@dimen/biometric_auth_pattern_view_size"
+ android:layout_height="@dimen/biometric_auth_pattern_view_size"/>
<TextView
android:id="@+id/error"
diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml
index bc97e51..8cf4f4d 100644
--- a/packages/SystemUI/res/layout/chipbar.xml
+++ b/packages/SystemUI/res/layout/chipbar.xml
@@ -23,6 +23,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
+ <!-- Extra marginBottom to give room for the drop shadow. -->
<LinearLayout
android:id="@+id/chipbar_inner"
android:orientation="horizontal"
@@ -33,6 +34,8 @@
android:layout_marginTop="20dp"
android:layout_marginStart="@dimen/notification_side_paddings"
android:layout_marginEnd="@dimen/notification_side_paddings"
+ android:translationZ="4dp"
+ android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:gravity="center_vertical"
android:alpha="0.0"
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 9134f96..eec3b11 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -32,26 +32,26 @@
android:elevation="4dp"
android:background="@drawable/action_chip_container_background"
android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
- app:layout_constraintBottom_toBottomOf="@+id/actions_container"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/actions_container"
- app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
+ app:layout_constraintEnd_toEndOf="@+id/actions_container"
+ app:layout_constraintBottom_toBottomOf="parent"/>
<HorizontalScrollView
android:id="@+id/actions_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
- android:paddingEnd="@dimen/overlay_action_container_padding_right"
+ android:paddingEnd="@dimen/overlay_action_container_padding_end"
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:scrollbars="none"
- android:layout_marginBottom="4dp"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintWidth_percent="1.0"
app:layout_constraintWidth_max="wrap"
- app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/preview_border"
- app:layout_constraintEnd_toEndOf="parent">
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
<LinearLayout
android:id="@+id/actions"
android:layout_width="wrap_content"
@@ -69,44 +69,30 @@
android:id="@+id/preview_border"
android:layout_width="0dp"
android:layout_height="0dp"
- android:layout_marginStart="@dimen/overlay_offset_x"
- android:layout_marginBottom="12dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginStart="@dimen/overlay_preview_container_margin"
+ android:layout_marginTop="@dimen/overlay_border_width_neg"
+ android:layout_marginEnd="@dimen/overlay_border_width_neg"
+ android:layout_marginBottom="@dimen/overlay_preview_container_margin"
android:elevation="7dp"
- app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
- app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
- android:background="@drawable/overlay_border"/>
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/clipboard_preview_end"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:barrierMargin="@dimen/overlay_border_width"
- app:barrierDirection="end"
- app:constraint_referenced_ids="clipboard_preview"/>
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/clipboard_preview_top"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:barrierDirection="top"
- app:barrierMargin="@dimen/overlay_border_width_neg"
- app:constraint_referenced_ids="clipboard_preview"/>
+ android:background="@drawable/overlay_border"
+ app:layout_constraintStart_toStartOf="@id/actions_container_background"
+ app:layout_constraintTop_toTopOf="@id/clipboard_preview"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
<FrameLayout
android:id="@+id/clipboard_preview"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/overlay_border_width"
+ android:layout_marginBottom="@dimen/overlay_border_width"
+ android:layout_gravity="center"
android:elevation="7dp"
android:background="@drawable/overlay_preview_background"
android:clipChildren="true"
android:clipToOutline="true"
android:clipToPadding="true"
- android:layout_width="@dimen/clipboard_preview_size"
- android:layout_margin="@dimen/overlay_border_width"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintBottom_toBottomOf="@id/preview_border"
app:layout_constraintStart_toStartOf="@id/preview_border"
- app:layout_constraintEnd_toEndOf="@id/preview_border"
- app:layout_constraintTop_toTopOf="@id/preview_border">
+ app:layout_constraintBottom_toBottomOf="@id/preview_border">
<TextView android:id="@+id/text_preview"
android:textFontWeight="500"
android:padding="8dp"
diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
deleted file mode 100644
index 1a1fc75..0000000
--- a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
+++ /dev/null
@@ -1,160 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ 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.
- -->
-<com.android.systemui.screenshot.DraggableConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/clipboard_ui"
- android:theme="@style/FloatingOverlay"
- android:alpha="0"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:contentDescription="@string/clipboard_overlay_window_name">
- <ImageView
- android:id="@+id/actions_container_background"
- android:visibility="gone"
- android:layout_height="0dp"
- android:layout_width="0dp"
- android:elevation="4dp"
- android:background="@drawable/action_chip_container_background"
- android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
- app:layout_constraintBottom_toBottomOf="@+id/actions_container"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="@+id/actions_container"
- app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
- <HorizontalScrollView
- android:id="@+id/actions_container"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
- android:paddingEnd="@dimen/overlay_action_container_padding_right"
- android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
- android:elevation="4dp"
- android:scrollbars="none"
- android:layout_marginBottom="4dp"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintWidth_percent="1.0"
- app:layout_constraintWidth_max="wrap"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/preview_border"
- app:layout_constraintEnd_toEndOf="parent">
- <LinearLayout
- android:id="@+id/actions"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true">
- <include layout="@layout/overlay_action_chip"
- android:id="@+id/share_chip"/>
- <include layout="@layout/overlay_action_chip"
- android:id="@+id/remote_copy_chip"/>
- <include layout="@layout/overlay_action_chip"
- android:id="@+id/edit_chip"/>
- </LinearLayout>
- </HorizontalScrollView>
- <View
- android:id="@+id/preview_border"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_marginStart="@dimen/overlay_offset_x"
- android:layout_marginBottom="12dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- android:elevation="7dp"
- app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
- app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
- android:background="@drawable/overlay_border"/>
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/clipboard_preview_end"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:barrierMargin="@dimen/overlay_border_width"
- app:barrierDirection="end"
- app:constraint_referenced_ids="clipboard_preview"/>
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/clipboard_preview_top"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:barrierDirection="top"
- app:barrierMargin="@dimen/overlay_border_width_neg"
- app:constraint_referenced_ids="clipboard_preview"/>
- <FrameLayout
- android:id="@+id/clipboard_preview"
- android:elevation="7dp"
- android:background="@drawable/overlay_preview_background"
- android:clipChildren="true"
- android:clipToOutline="true"
- android:clipToPadding="true"
- android:layout_width="@dimen/clipboard_preview_size"
- android:layout_margin="@dimen/overlay_border_width"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- app:layout_constraintBottom_toBottomOf="@id/preview_border"
- app:layout_constraintStart_toStartOf="@id/preview_border"
- app:layout_constraintEnd_toEndOf="@id/preview_border"
- app:layout_constraintTop_toTopOf="@id/preview_border">
- <TextView android:id="@+id/text_preview"
- android:textFontWeight="500"
- android:padding="8dp"
- android:gravity="center|start"
- android:ellipsize="end"
- android:autoSizeTextType="uniform"
- android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
- android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
- android:textColor="?attr/overlayButtonTextColor"
- android:textColorLink="?attr/overlayButtonTextColor"
- android:background="?androidprv:attr/colorAccentSecondary"
- android:layout_width="@dimen/clipboard_preview_size"
- android:layout_height="@dimen/clipboard_preview_size"/>
- <ImageView
- android:id="@+id/image_preview"
- android:scaleType="fitCenter"
- android:adjustViewBounds="true"
- android:contentDescription="@string/clipboard_image_preview"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- <TextView
- android:id="@+id/hidden_preview"
- android:visibility="gone"
- android:textFontWeight="500"
- android:padding="8dp"
- android:gravity="center"
- android:textSize="14sp"
- android:textColor="?attr/overlayButtonTextColor"
- android:background="?androidprv:attr/colorAccentSecondary"
- android:layout_width="@dimen/clipboard_preview_size"
- android:layout_height="@dimen/clipboard_preview_size"/>
- </FrameLayout>
- <FrameLayout
- android:id="@+id/dismiss_button"
- android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
- android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
- android:elevation="10dp"
- android:visibility="gone"
- android:alpha="0"
- app:layout_constraintStart_toEndOf="@id/clipboard_preview"
- app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
- app:layout_constraintTop_toTopOf="@id/clipboard_preview"
- app:layout_constraintBottom_toTopOf="@id/clipboard_preview"
- android:contentDescription="@string/clipboard_dismiss_description">
- <ImageView
- android:id="@+id/dismiss_image"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="@dimen/overlay_dismiss_button_margin"
- android:src="@drawable/overlay_cancel"/>
- </FrameLayout>
-</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index a565988..d689828 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -148,9 +148,4 @@
<include layout="@layout/ongoing_privacy_chip"/>
</FrameLayout>
- <Space
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:id="@+id/space"
- />
</com.android.systemui.util.NoRemeasureMotionLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml
index de96e97..446bb01 100644
--- a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml
@@ -20,7 +20,7 @@
android:layout_width="wrap_content"
android:paddingVertical="@dimen/dream_overlay_complication_home_controls_padding">
- <ImageView
+ <com.android.systemui.common.ui.view.LaunchableImageView
android:id="@+id/home_controls_chip"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_width="@dimen/keyguard_affordance_fixed_width"
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 9add32c..885e5e2 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -57,6 +57,7 @@
android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+ android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop"
android:src="@drawable/ic_alarm"
android:tint="@android:color/white"
android:visibility="gone"
@@ -67,6 +68,7 @@
android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+ android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop"
android:src="@drawable/ic_qs_dnd_on"
android:tint="@android:color/white"
android:visibility="gone"
@@ -77,6 +79,7 @@
android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
+ android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop"
android:src="@drawable/ic_signal_wifi_off"
android:visibility="gone"
android:contentDescription="@string/dream_overlay_status_bar_wifi_off" />
diff --git a/packages/SystemUI/res/layout/emergency_cryptkeeper_text.xml b/packages/SystemUI/res/layout/emergency_cryptkeeper_text.xml
deleted file mode 100644
index 0a1730a..0000000
--- a/packages/SystemUI/res/layout/emergency_cryptkeeper_text.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2016 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.statusbar.policy.EmergencyCryptkeeperText
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/emergency_cryptkeeper_text"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:textAppearance="@style/TextAppearance.StatusBar.Clock"
- android:paddingStart="6dp"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:gravity="center_vertical|start"
- />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/global_actions_grid_lite.xml b/packages/SystemUI/res/layout/global_actions_grid_lite.xml
index 5588fd3..a64c9ae 100644
--- a/packages/SystemUI/res/layout/global_actions_grid_lite.xml
+++ b/packages/SystemUI/res/layout/global_actions_grid_lite.xml
@@ -33,7 +33,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_weight="1">
- <androidx.constraintlayout.widget.ConstraintLayout
+ <com.android.systemui.common.ui.view.LaunchableConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@android:id/list"
@@ -55,6 +55,6 @@
app:flow_horizontalGap="@dimen/global_actions_lite_padding"
app:flow_verticalGap="@dimen/global_actions_lite_padding"
app:flow_horizontalStyle="packed"/>
- </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.android.systemui.common.ui.view.LaunchableConstraintLayout>
</com.android.systemui.globalactions.GlobalActionsLayoutLite>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 6120863..3f95515 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -67,6 +67,7 @@
android:scaleType="center"
android:tint="?android:attr/textColorPrimary"
android:background="@drawable/keyguard_bottom_affordance_bg"
+ android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset"
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
android:visibility="gone" />
@@ -79,6 +80,7 @@
android:scaleType="center"
android:tint="?android:attr/textColorPrimary"
android:background="@drawable/keyguard_bottom_affordance_bg"
+ android:foreground="@drawable/keyguard_bottom_affordance_selected_border"
android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset"
android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
android:visibility="gone" />
diff --git a/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml b/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
index 6f33623..07c428b 100644
--- a/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
+++ b/packages/SystemUI/res/layout/keyguard_qs_user_switch.xml
@@ -24,7 +24,7 @@
android:layout_gravity="end">
<!-- We add a background behind the UserAvatarView with the same color and with a circular shape
so that this view can be expanded into a Dialog or an Activity. -->
- <FrameLayout
+ <com.android.systemui.animation.LaunchableFrameLayout
android:id="@+id/kg_multi_user_avatar_with_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -42,5 +42,5 @@
systemui:framePadding="0dp"
systemui:frameWidth="0dp">
</com.android.systemui.statusbar.phone.UserAvatarView>
- </FrameLayout>
+ </com.android.systemui.animation.LaunchableFrameLayout>
</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml
new file mode 100644
index 0000000..89d88fe
--- /dev/null
+++ b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ ~
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="52dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:background="@drawable/keyguard_settings_popup_menu_background"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:paddingVertical="16dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_marginEnd="16dp"
+ android:tint="?android:attr/textColorPrimary"
+ android:importantForAccessibility="no"
+ tools:ignore="UseAppTint" />
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="14sp"
+ android:maxLines="1"
+ android:ellipsize="end" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index b76de5a..e182a6a 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -24,6 +24,7 @@
android:orientation="vertical">
<LinearLayout
+ android:id="@+id/media_metadata_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml
new file mode 100644
index 0000000..101fad9
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_recommendation_view.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<!-- Layout for media recommendation item inside QSPanel carousel -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Album cover -->
+ <ImageView
+ android:id="@+id/media_cover"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:translationZ="0dp"
+ android:scaleType="centerCrop"
+ android:adjustViewBounds="true"
+ android:clipToOutline="true"
+ android:background="@drawable/bg_smartspace_media_item"/>
+
+ <!-- App icon -->
+ <com.android.internal.widget.CachingIconView
+ android:id="@+id/media_rec_app_icon"
+ android:layout_width="@dimen/qs_media_rec_icon_top_margin"
+ android:layout_height="@dimen/qs_media_rec_icon_top_margin"
+ android:layout_marginStart="@dimen/qs_media_info_spacing"
+ android:layout_marginTop="@dimen/qs_media_info_spacing"/>
+
+ <!-- Artist name -->
+ <TextView
+ android:id="@+id/media_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/qs_media_info_spacing"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginBottom="@dimen/qs_media_rec_album_title_bottom_margin"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:singleLine="true"
+ android:textSize="12sp"
+ android:gravity="top"
+ android:layout_gravity="bottom"/>
+
+ <!-- Album name -->
+ <TextView
+ android:id="@+id/media_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/qs_media_rec_album_subtitle_height"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginStart="@dimen/qs_media_info_spacing"
+ android:layout_marginBottom="@dimen/qs_media_info_spacing"
+ android:fontFamily="@*android:string/config_headlineFontFamily"
+ android:singleLine="true"
+ android:textSize="11sp"
+ android:gravity="center_vertical"
+ android:layout_gravity="bottom"/>
+</merge>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_recommendations.xml b/packages/SystemUI/res/layout/media_recommendations.xml
new file mode 100644
index 0000000..65fc19c
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_recommendations.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<!-- Layout for media recommendations inside QSPanel carousel -->
+<com.android.systemui.util.animation.TransitionLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/media_recommendations_updated"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:forceHasOverlappingRendering="false"
+ android:background="@drawable/qs_media_background"
+ android:theme="@style/MediaPlayer">
+
+ <!-- This view just ensures the full media player is a certain height. -->
+ <View
+ android:id="@+id/sizing_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/qs_media_session_height_expanded" />
+
+ <TextView
+ android:id="@+id/media_rec_title"
+ style="@style/MediaPlayer.Recommendation.Header"
+ android:text="@string/controls_media_smartspace_rec_header"/>
+
+ <FrameLayout
+ android:id="@+id/media_cover1_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ >
+
+ <include
+ layout="@layout/media_recommendation_view"/>
+
+ </FrameLayout>
+
+
+ <FrameLayout
+ android:id="@+id/media_cover2_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ >
+
+ <include
+ layout="@layout/media_recommendation_view"/>
+
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/media_cover3_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ >
+
+ <include
+ layout="@layout/media_recommendation_view"/>
+
+ </FrameLayout>
+
+ <include
+ layout="@layout/media_long_press_menu" />
+
+</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 95aefab..f2e114b 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -106,7 +106,7 @@
app:layout_constrainedWidth="true"
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
app:layout_constraintHeight_min="@dimen/min_clickable_item_size">
- <LinearLayout
+ <com.android.systemui.common.ui.view.LaunchableLinearLayout
android:id="@+id/media_seamless_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -135,7 +135,7 @@
android:textDirection="locale"
android:textSize="12sp"
android:lineHeight="16sp" />
- </LinearLayout>
+ </com.android.systemui.common.ui.view.LaunchableLinearLayout>
</LinearLayout>
<!-- Song name -->
@@ -147,6 +147,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
+ <!-- Explicit Indicator -->
+ <com.android.internal.widget.CachingIconView
+ android:id="@+id/media_explicit_indicator"
+ android:layout_width="@dimen/qs_media_explicit_indicator_icon_size"
+ android:layout_height="@dimen/qs_media_explicit_indicator_icon_size"
+ android:src="@drawable/ic_media_explicit_indicator"
+ />
+
<!-- Artist name -->
<TextView
android:id="@+id/header_artist"
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index 21d12c2..4483db8 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -27,6 +27,14 @@
android:layout_height="wrap_content"
/>
+ <com.android.systemui.media.taptotransfer.receiver.ReceiverChipRippleView
+ android:id="@+id/icon_glow_ripple"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+
+ <!-- Add a bottom margin to avoid the glow of the icon ripple from being cropped by screen
+ bounds while animating with the icon -->
<com.android.internal.widget.CachingIconView
android:id="@+id/app_icon"
android:background="@drawable/media_ttt_chip_background_receiver"
@@ -34,6 +42,7 @@
android:layout_height="@dimen/media_ttt_icon_size_receiver"
android:layout_gravity="center|bottom"
android:alpha="0.0"
+ android:layout_marginBottom="@dimen/media_ttt_receiver_icon_bottom_margin"
/>
</FrameLayout>
diff --git a/packages/SystemUI/res/layout/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml
index c949ba0..18d231c 100644
--- a/packages/SystemUI/res/layout/ongoing_call_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml
@@ -23,7 +23,7 @@
android:layout_gravity="center_vertical|start"
android:layout_marginStart="5dp"
>
- <LinearLayout
+ <com.android.systemui.common.ui.view.LaunchableLinearLayout
android:id="@+id/ongoing_call_chip_background"
android:layout_width="wrap_content"
android:layout_height="@dimen/ongoing_appops_chip_height"
@@ -55,5 +55,5 @@
android:textColor="?android:attr/colorPrimary"
/>
- </LinearLayout>
+ </com.android.systemui.common.ui.view.LaunchableLinearLayout>
</FrameLayout>
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
index 5aa6080..d1a2cf4 100644
--- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -25,6 +25,7 @@
android:focusable="true"
android:clipChildren="false"
android:clipToPadding="false"
+ android:paddingStart="8dp"
>
<LinearLayout
diff --git a/packages/SystemUI/res/layout/screenshot_detection_notice.xml b/packages/SystemUI/res/layout/screenshot_detection_notice.xml
new file mode 100644
index 0000000..fc936c0
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_detection_notice.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/screenshot_detection_notice"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:padding="12dp"
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/screenshot_detection_notice_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lineHeight="24sp"
+ android:textSize="18sp" />
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index e4e0bd4..a748e29 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -27,26 +27,26 @@
android:elevation="4dp"
android:background="@drawable/action_chip_container_background"
android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
- app:layout_constraintBottom_toBottomOf="@+id/actions_container"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/actions_container"
- app:layout_constraintEnd_toEndOf="@+id/actions_container"/>
+ app:layout_constraintEnd_toEndOf="@+id/actions_container"
+ app:layout_constraintBottom_toTopOf="@id/guideline"/>
<HorizontalScrollView
android:id="@+id/actions_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
- android:layout_marginBottom="4dp"
- android:paddingEnd="@dimen/overlay_action_container_padding_right"
+ android:paddingEnd="@dimen/overlay_action_container_padding_end"
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:scrollbars="none"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintWidth_percent="1.0"
app:layout_constraintWidth_max="wrap"
- app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"
app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
- app:layout_constraintEnd_toEndOf="parent">
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
<LinearLayout
android:id="@+id/screenshot_actions"
android:layout_width="wrap_content"
@@ -64,35 +64,24 @@
android:id="@+id/screenshot_preview_border"
android:layout_width="0dp"
android:layout_height="0dp"
- android:layout_marginStart="@dimen/overlay_offset_x"
- android:layout_marginBottom="12dp"
+ android:layout_marginStart="@dimen/overlay_preview_container_margin"
+ android:layout_marginTop="@dimen/overlay_border_width_neg"
+ android:layout_marginEnd="@dimen/overlay_border_width_neg"
+ android:layout_marginBottom="@dimen/overlay_preview_container_margin"
android:elevation="7dp"
android:alpha="0"
android:background="@drawable/overlay_border"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"
- app:layout_constraintEnd_toEndOf="@id/screenshot_preview_end"
- app:layout_constraintTop_toTopOf="@id/screenshot_preview_top"/>
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/screenshot_preview_end"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:barrierMargin="@dimen/overlay_border_width"
- app:barrierDirection="end"
- app:constraint_referenced_ids="screenshot_preview"/>
- <androidx.constraintlayout.widget.Barrier
- android:id="@+id/screenshot_preview_top"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:barrierDirection="top"
- app:barrierMargin="@dimen/overlay_border_width_neg"
- app:constraint_referenced_ids="screenshot_preview"/>
+ app:layout_constraintStart_toStartOf="@id/actions_container_background"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
<ImageView
android:id="@+id/screenshot_preview"
android:visibility="invisible"
android:layout_width="@dimen/overlay_x_scale"
- android:layout_margin="@dimen/overlay_border_width"
android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/overlay_border_width"
+ android:layout_marginBottom="@dimen/overlay_border_width"
android:layout_gravity="center"
android:elevation="7dp"
android:contentDescription="@string/screenshot_edit_description"
@@ -100,20 +89,14 @@
android:background="@drawable/overlay_preview_background"
android:adjustViewBounds="true"
android:clickable="true"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
- app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
- app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/>
+ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/>
<ImageView
android:id="@+id/screenshot_badge"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:padding="4dp"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
android:visibility="gone"
- android:background="@drawable/overlay_badge_background"
android:elevation="8dp"
- android:src="@drawable/overlay_cancel"
app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
<FrameLayout
@@ -144,57 +127,30 @@
app:layout_constraintTop_toTopOf="@id/screenshot_preview"
android:elevation="7dp"/>
- <androidx.constraintlayout.widget.ConstraintLayout
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_end="0dp" />
+
+ <FrameLayout
android:id="@+id/screenshot_message_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
- android:layout_marginVertical="4dp"
- android:paddingHorizontal="@dimen/overlay_action_container_padding_right"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:background="@drawable/action_chip_container_background"
android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toBottomOf="parent">
-
- <ImageView
- android:id="@+id/screenshot_message_icon"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:paddingEnd="4dp"
- android:src="@drawable/ic_work_app_badge"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/screenshot_message_content"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"/>
-
- <TextView
- android:id="@+id/screenshot_message_content"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_gravity="start"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
- app:layout_constraintEnd_toStartOf="@id/message_dismiss_button"/>
-
- <FrameLayout
- android:id="@+id/message_dismiss_button"
- android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
- android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
- app:layout_constraintStart_toEndOf="@id/screenshot_message_content"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- android:contentDescription="@string/screenshot_dismiss_work_profile">
- <ImageView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_margin="@dimen/overlay_dismiss_button_margin"
- android:src="@drawable/overlay_cancel"/>
- </FrameLayout>
-
- </androidx.constraintlayout.widget.ConstraintLayout>
+ >
+ <include layout="@layout/screenshot_work_profile_first_run" />
+ <include layout="@layout/screenshot_detection_notice" />
+ </FrameLayout>
</com.android.systemui.screenshot.DraggableConstraintLayout>
diff --git a/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
new file mode 100644
index 0000000..392d845
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_work_profile_first_run.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/work_profile_first_run"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingStart="16dp"
+ android:paddingEnd="4dp"
+ android:paddingVertical="16dp"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/screenshot_message_icon"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_marginEnd="12dp"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_work_app_badge"/>
+
+ <TextView
+ android:id="@+id/screenshot_message_content"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="start|center_vertical"
+ android:textSize="18sp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:lineHeight="24sp"
+ />
+
+ <FrameLayout
+ android:id="@+id/message_dismiss_button"
+ android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+ android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+ android:contentDescription="@string/screenshot_dismiss_work_profile">
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center"
+ android:src="@drawable/overlay_cancel"/>
+ </FrameLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index 3b71dc3..64aa629 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -149,11 +149,4 @@
</FrameLayout>
</LinearLayout>
- <ViewStub
- android:id="@+id/emergency_cryptkeeper_text"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout="@layout/emergency_cryptkeeper_text"
- />
-
</com.android.systemui.statusbar.phone.PhoneStatusBarView>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 159323a..3c860a9 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -26,6 +26,11 @@
android:layout_height="match_parent"
android:background="@android:color/transparent">
+ <com.android.systemui.common.ui.view.LongPressHandlingView
+ android:id="@+id/keyguard_long_press"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
<ViewStub
android:id="@+id/keyguard_qs_user_switch_stub"
android:layout="@layout/keyguard_qs_user_switch"
diff --git a/packages/SystemUI/res/layout/status_bar_notification_footer.xml b/packages/SystemUI/res/layout/status_bar_notification_footer.xml
index bbb8df1c..db94c92 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_footer.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_footer.xml
@@ -26,6 +26,17 @@
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/unlock_prompt_footer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"
+ android:drawablePadding="8dp"
+ android:visibility="gone"
+ android:textAppearance="?android:attr/textAppearanceButton"
+ android:text="@string/unlock_to_see_notif_text"/>
<com.android.systemui.statusbar.notification.row.FooterViewButton
style="@style/TextAppearance.NotificationSectionHeaderButton"
android:id="@+id/manage_text"
diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
index fa9d739..7eaed43 100644
--- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
+++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
@@ -46,7 +46,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:flow_horizontalBias="0.5"
app:flow_verticalAlign="center"
- app:flow_wrapMode="chain"
+ app:flow_wrapMode="chain2"
app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap"
app:flow_verticalGap="44dp"
app:flow_horizontalStyle="packed"/>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 49ef330..fff2544 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -40,6 +40,10 @@
<dimen name="biometric_dialog_button_negative_max_width">140dp</dimen>
<dimen name="biometric_dialog_button_positive_max_width">116dp</dimen>
+ <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->
+ <dimen name="biometric_auth_pattern_view_size">248dp</dimen>
+ <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen>
+
<dimen name="global_actions_power_dialog_item_height">130dp</dimen>
<dimen name="global_actions_power_dialog_item_bottom_margin">35dp</dimen>
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index aefd998..a0e721e 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -29,11 +29,11 @@
<style name="AuthCredentialPatternContainerStyle">
<item name="android:gravity">center</item>
- <item name="android:maxHeight">320dp</item>
- <item name="android:maxWidth">320dp</item>
- <item name="android:minHeight">200dp</item>
- <item name="android:minWidth">200dp</item>
- <item name="android:paddingHorizontal">60dp</item>
+ <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:paddingHorizontal">32dp</item>
<item name="android:paddingVertical">20dp</item>
</style>
diff --git a/packages/SystemUI/res/values-sw360dp/dimens.xml b/packages/SystemUI/res/values-sw360dp/dimens.xml
index 65ca70b..03365b3 100644
--- a/packages/SystemUI/res/values-sw360dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw360dp/dimens.xml
@@ -25,5 +25,8 @@
<!-- Home Controls -->
<dimen name="global_actions_side_margin">12dp</dimen>
+
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">298dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw392dp-land/dimens.xml b/packages/SystemUI/res/values-sw392dp-land/dimens.xml
new file mode 100644
index 0000000..1e26a69
--- /dev/null
+++ b/packages/SystemUI/res/values-sw392dp-land/dimens.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ 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.
+ -->
+
+<resources>
+ <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->
+ <dimen name="biometric_auth_pattern_view_size">248dp</dimen>
+ <dimen name="biometric_auth_pattern_view_max_size">248dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw392dp/dimens.xml b/packages/SystemUI/res/values-sw392dp/dimens.xml
index 78279ca..96af3c1 100644
--- a/packages/SystemUI/res/values-sw392dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw392dp/dimens.xml
@@ -24,5 +24,8 @@
<!-- Home Controls -->
<dimen name="global_actions_side_margin">16dp</dimen>
+
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">298dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw410dp-land/dimens.xml b/packages/SystemUI/res/values-sw410dp-land/dimens.xml
new file mode 100644
index 0000000..c4d9b9b
--- /dev/null
+++ b/packages/SystemUI/res/values-sw410dp-land/dimens.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ 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.
+ -->
+
+<resources>
+ <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size -->
+ <dimen name="biometric_auth_pattern_view_size">248dp</dimen>
+ <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw410dp/dimens.xml b/packages/SystemUI/res/values-sw410dp/dimens.xml
index 7da47e5..ff6e005 100644
--- a/packages/SystemUI/res/values-sw410dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw410dp/dimens.xml
@@ -27,4 +27,6 @@
<dimen name="global_actions_grid_item_side_margin">12dp</dimen>
<dimen name="global_actions_grid_item_height">72dp</dimen>
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 6c7cab5..5d78e4e 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -28,6 +28,7 @@
<!-- QS-->
<dimen name="qs_panel_padding_top">16dp</dimen>
+ <dimen name="qs_panel_padding">24dp</dimen>
<dimen name="qs_content_horizontal_padding">24dp</dimen>
<dimen name="qs_horizontal_margin">24dp</dimen>
<!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2,
diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml
index 8148d3d..c535c64 100644
--- a/packages/SystemUI/res/values-sw600dp-land/styles.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml
@@ -18,10 +18,10 @@
<style name="AuthCredentialPatternContainerStyle">
<item name="android:gravity">center</item>
- <item name="android:maxHeight">420dp</item>
- <item name="android:maxWidth">420dp</item>
- <item name="android:minHeight">200dp</item>
- <item name="android:minWidth">200dp</item>
+ <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
<item name="android:paddingHorizontal">120dp</item>
<item name="android:paddingVertical">40dp</item>
</style>
diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml
index 771de08..32eefa7 100644
--- a/packages/SystemUI/res/values-sw600dp-port/styles.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml
@@ -26,10 +26,10 @@
<style name="AuthCredentialPatternContainerStyle">
<item name="android:gravity">center</item>
- <item name="android:maxHeight">420dp</item>
- <item name="android:maxWidth">420dp</item>
- <item name="android:minHeight">200dp</item>
- <item name="android:minWidth">200dp</item>
+ <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
<item name="android:paddingHorizontal">180dp</item>
<item name="android:paddingVertical">80dp</item>
</style>
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index f4434e8..ea3c012 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -36,4 +36,15 @@
<integer name="qs_security_footer_maxLines">1</integer>
<bool name="config_use_large_screen_shade_header">true</bool>
+
+ <!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a
+ string with two parts: the ID of the slot and the comma-delimited list of affordance IDs,
+ separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The
+ default is displayed by System UI as long as the user hasn't made a different choice for that
+ slot. If the user did make a choice, even if the choice is the "None" option, the default is
+ ignored. -->
+ <string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
+ <item>bottom_start:home</item>
+ <item>bottom_end:create_note</item>
+ </string-array>
</resources>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 599bf30..db7fb48 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -30,10 +30,6 @@
<!-- Margin on the left side of the carrier text on Keyguard -->
<dimen name="keyguard_carrier_text_margin">24dp</dimen>
- <!-- The width/height of the phone/camera/unlock icon on keyguard. -->
- <dimen name="keyguard_affordance_height">80dp</dimen>
- <dimen name="keyguard_affordance_width">120dp</dimen>
-
<!-- Screen pinning request width -->
<dimen name="screen_pinning_request_width">400dp</dimen>
<!-- Screen pinning request bottom button circle widths -->
@@ -92,4 +88,6 @@
<dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
<dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 3fc59e3..122806a 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -27,7 +27,7 @@
<dimen name="status_view_margin_horizontal">24dp</dimen>
- <dimen name="qs_media_session_height_expanded">251dp</dimen>
+ <dimen name="qs_media_session_height_expanded">184dp</dimen>
<dimen name="qs_content_horizontal_padding">40dp</dimen>
<dimen name="qs_horizontal_margin">40dp</dimen>
<!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2,
@@ -36,8 +36,8 @@
<dimen name="qs_tiles_page_horizontal_margin">20dp</dimen>
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
- <dimen name="qs_media_rec_icon_top_margin">27dp</dimen>
- <dimen name="qs_media_rec_album_size">152dp</dimen>
+ <dimen name="qs_media_rec_icon_top_margin">16dp</dimen>
+ <dimen name="qs_media_rec_album_size">112dp</dimen>
<dimen name="qs_media_rec_album_side_margin">16dp</dimen>
<dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml
index f9ed67d..6a70ebd 100644
--- a/packages/SystemUI/res/values-sw720dp-land/styles.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml
@@ -18,10 +18,10 @@
<style name="AuthCredentialPatternContainerStyle">
<item name="android:gravity">center</item>
- <item name="android:maxHeight">420dp</item>
- <item name="android:maxWidth">420dp</item>
- <item name="android:minHeight">200dp</item>
- <item name="android:minWidth">200dp</item>
+ <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
<item name="android:paddingHorizontal">120dp</item>
<item name="android:paddingVertical">40dp</item>
</style>
diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml
index 78d299c..0a46e08 100644
--- a/packages/SystemUI/res/values-sw720dp-port/styles.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml
@@ -26,10 +26,10 @@
<style name="AuthCredentialPatternContainerStyle">
<item name="android:gravity">center</item>
- <item name="android:maxHeight">420dp</item>
- <item name="android:maxWidth">420dp</item>
- <item name="android:minHeight">200dp</item>
- <item name="android:minWidth">200dp</item>
+ <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
<item name="android:paddingHorizontal">240dp</item>
<item name="android:paddingVertical">120dp</item>
</style>
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index 0705017..927059a 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -22,5 +22,8 @@
<dimen name="controls_padding_horizontal">75dp</dimen>
<dimen name="large_screen_shade_header_height">56dp</dimen>
+
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values-sw800dp/dimens.xml b/packages/SystemUI/res/values-sw800dp/dimens.xml
new file mode 100644
index 0000000..0d82217
--- /dev/null
+++ b/packages/SystemUI/res/values-sw800dp/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds. -->
+<resources>
+
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7d72598..371f001 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -437,6 +437,11 @@
This name is in the ComponentName flattened format (package/class) -->
<string name="config_screenshotEditor" translatable="false"></string>
+ <!-- ComponentName for the file browsing app that the system would expect to be used in work
+ profile. The icon for this app will be shown to the user when informing them that a
+ screenshot has been saved to work profile. If blank, a default icon will be shown. -->
+ <string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string>
+
<!-- Remote copy default activity. Must handle REMOTE_COPY_ACTION intents.
This name is in the ComponentName flattened format (package/class) -->
<string name="config_remoteCopyPackage" translatable="false"></string>
@@ -663,6 +668,16 @@
<item>17</item> <!-- WAKE_REASON_BIOMETRIC -->
</integer-array>
+ <!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN)
+ means systemui will try listening on all postures.
+ 0 : DEVICE_POSTURE_UNKNOWN
+ 1 : DEVICE_POSTURE_CLOSED
+ 2 : DEVICE_POSTURE_HALF_OPENED
+ 3 : DEVICE_POSTURE_OPENED
+ 4 : DEVICE_POSTURE_FLIPPED
+ -->
+ <integer name="config_face_auth_supported_posture">0</integer>
+
<!-- Whether the communal service should be enabled -->
<bool name="config_communalServiceEnabled">false</bool>
@@ -809,4 +824,12 @@
<item>bottom_end:wallet</item>
</string-array>
+ <!-- Package name for the app that implements the wallpaper picker. -->
+ <string name="config_wallpaperPickerPackage" translatable="false">
+ com.android.wallpaper
+ </string>
+
+ <!-- Whether the floating rotation button should be on the left/right in the device's natural
+ orientation -->
+ <bool name="floating_rotation_button_position_left">true</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ecb6560..2b0021b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -334,15 +334,22 @@
<dimen name="overlay_action_chip_spacing">8dp</dimen>
<dimen name="overlay_action_chip_text_size">14sp</dimen>
<dimen name="overlay_offset_x">16dp</dimen>
+ <!-- Used for both start and bottom margin of the preview, relative to the action container -->
+ <dimen name="overlay_preview_container_margin">8dp</dimen>
<dimen name="overlay_action_container_margin_horizontal">8dp</dimen>
+ <dimen name="overlay_action_container_margin_bottom">6dp</dimen>
<dimen name="overlay_bg_protection_height">242dp</dimen>
<dimen name="overlay_action_container_corner_radius">18dp</dimen>
<dimen name="overlay_action_container_padding_vertical">4dp</dimen>
<dimen name="overlay_action_container_padding_right">8dp</dimen>
+ <dimen name="overlay_action_container_padding_end">8dp</dimen>
<dimen name="overlay_dismiss_button_tappable_size">48dp</dimen>
<dimen name="overlay_dismiss_button_margin">8dp</dimen>
+ <!-- must be kept aligned with overlay_border_width_neg, below;
+ overlay_border_width = overlay_border_width_neg * -1 -->
<dimen name="overlay_border_width">4dp</dimen>
- <!-- need a negative margin for some of the constraints. should be overlay_border_width * -1 -->
+ <!-- some constraints use a negative margin. must be aligned with overlay_border_width, above;
+ overlay_border_width_neg = overlay_border_width * -1 -->
<dimen name="overlay_border_width_neg">-4dp</dimen>
<dimen name="clipboard_preview_size">@dimen/overlay_x_scale</dimen>
@@ -755,12 +762,10 @@
<dimen name="go_to_full_shade_appearing_translation">200dp</dimen>
<!-- The width/height of the keyguard bottom area icon view on keyguard. -->
- <dimen name="keyguard_affordance_height">48dp</dimen>
- <dimen name="keyguard_affordance_width">48dp</dimen>
-
<dimen name="keyguard_affordance_fixed_height">48dp</dimen>
<dimen name="keyguard_affordance_fixed_width">48dp</dimen>
<dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
+
<!-- Amount the button should shake when it's not long-pressed for long enough. -->
<dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>
@@ -966,6 +971,10 @@
<!-- Biometric Auth Credential values -->
<dimen name="biometric_auth_icon_size">48dp</dimen>
+ <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
+ <dimen name="biometric_auth_pattern_view_size">348dp</dimen>
+ <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen>
+
<!-- Starting text size in sp of batteryLevel for wireless charging animation -->
<item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen">
0
@@ -1030,8 +1039,6 @@
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
- <!-- Size of the RAT type for CellularTile -->
-
<!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_padding">16dp</dimen>
<dimen name="qs_media_album_radius">14dp</dimen>
@@ -1046,6 +1053,7 @@
<dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
<dimen name="qs_media_enabled_seekbar_height">2dp</dimen>
<dimen name="qs_media_app_icon_size">24dp</dimen>
+ <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen>
<dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen>
<dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen>
@@ -1060,8 +1068,13 @@
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
<dimen name="qs_media_rec_icon_top_margin">16dp</dimen>
<dimen name="qs_media_rec_album_size">88dp</dimen>
+ <dimen name="qs_media_rec_album_width">110dp</dimen>
+ <dimen name="qs_media_rec_album_height_expanded">108dp</dimen>
+ <dimen name="qs_media_rec_album_height_collapsed">77dp</dimen>
<dimen name="qs_media_rec_album_side_margin">16dp</dimen>
<dimen name="qs_media_rec_album_bottom_margin">8dp</dimen>
+ <dimen name="qs_media_rec_album_title_bottom_margin">22dp</dimen>
+ <dimen name="qs_media_rec_album_subtitle_height">12dp</dimen>
<!-- Media tap-to-transfer chip for sender device -->
<dimen name="media_ttt_chip_outer_padding">16dp</dimen>
@@ -1079,6 +1092,7 @@
(112 - 40) / 2 = 36dp -->
<dimen name="media_ttt_generic_icon_padding">36dp</dimen>
<dimen name="media_ttt_receiver_vert_translation">40dp</dimen>
+ <dimen name="media_ttt_receiver_icon_bottom_margin">10dp</dimen>
<!-- Window magnification -->
<dimen name="magnification_border_drag_size">35dp</dimen>
@@ -1279,6 +1293,15 @@
<!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
<dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+ <!-- LOCKSCREEN -> DREAMING transition: Amount to shift lockscreen content on entering -->
+ <dimen name="lockscreen_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+
+ <!-- GONE -> DREAMING transition: Amount to shift lockscreen content on entering -->
+ <dimen name="gone_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen>
+
+ <!-- LOCKSCREEN -> OCCLUDED transition: Amount to shift lockscreen content on entering -->
+ <dimen name="lockscreen_to_occluded_transition_lockscreen_translation_y">-40dp</dimen>
+
<!-- The amount of vertical offset for the keyguard during the full shade transition. -->
<dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
@@ -1617,6 +1640,8 @@
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+ <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen>
+ <dimen name="dream_overlay_status_bar_marginTop">22dp</dimen>
<!-- Default device corner radius, used for assist UI -->
<dimen name="config_rounded_mask_size">0px</dimen>
@@ -1628,4 +1653,10 @@
<dimen name="rear_display_animation_height">200dp</dimen>
<dimen name="rear_display_title_top_padding">24dp</dimen>
<dimen name="rear_display_title_bottom_padding">16dp</dimen>
+
+ <!--
+ Vertical distance between the pointer and the popup menu that shows up on the lock screen when
+ it is long-pressed.
+ -->
+ <dimen name="keyguard_long_press_settings_popup_vertical_offset">96dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index c5ffc94..6354752 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -33,8 +33,6 @@
<!-- Whether to show chipbar UI whenever the device is unlocked by ActiveUnlock. -->
<bool name="flag_active_unlock_chipbar">true</bool>
- <bool name="flag_smartspace">false</bool>
-
<!-- Whether the user switcher chip shows in the status bar. When true, the multi user
avatar will no longer show on the lockscreen -->
<bool name="flag_user_switcher_chip">false</bool>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 977adde..227b00e 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -240,7 +240,13 @@
<!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
<string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
<!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
- <string name="screenshot_work_profile_notification" translatable="false">Work screenshots are saved in the work <xliff:g id="app" example="Files">%1$s</xliff:g> app</string>
+ <string name="screenshot_work_profile_notification">Saved in <xliff:g id="app" example="Files">%1$s</xliff:g> in the work profile</string>
+ <!-- Default name referring to the app on the device that lets the user browse stored files. [CHAR LIMIT=NONE] -->
+ <string name="screenshot_default_files_app_name">Files</string>
+ <!-- A notice shown to the user to indicate that an app has detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
+ <string name="screenshot_detected_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> detected this screenshot.</string>
+ <!-- A notice shown to the user to indicate that multiple apps have detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
+ <string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string>
<!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
<string name="screenrecord_name">Screen Recorder</string>
@@ -1453,10 +1459,10 @@
<string name="notification_conversation_summary_low">No sound or vibration and appears lower in conversation section</string>
<!-- [CHAR LIMIT=150] Notification Importance title: normal importance level summary -->
- <string name="notification_channel_summary_default">May ring or vibrate based on phone settings</string>
+ <string name="notification_channel_summary_default">May ring or vibrate based on device settings</string>
<!-- [CHAR LIMIT=150] Conversation Notification Importance title: normal conversation level, with bubbling summary -->
- <string name="notification_channel_summary_default_with_bubbles">May ring or vibrate based on phone settings. Conversations from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubble by default.</string>
+ <string name="notification_channel_summary_default_with_bubbles">May ring or vibrate based on device settings. Conversations from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubble by default.</string>
<!-- [CHAR LIMIT=150] Notification Importance title: automatic importance level summary -->
<string name="notification_channel_summary_automatic">Have the system determine if this notification should make sound or vibration</string>
@@ -2236,6 +2242,14 @@
<!-- Removed control in management screen [CHAR LIMIT=20] -->
<string name="controls_removed">Removed</string>
+ <!-- Title for the dialog presented to the user to authorize this app to display a Device
+ controls panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=30] -->
+ <string name="controls_panel_authorization_title">Add <xliff:g id="appName" example="My app">%s</xliff:g>?</string>
+
+ <!-- Shows in a dialog presented to the user to authorize this app to display a Device controls
+ panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] -->
+ <string name="controls_panel_authorization">When you add <xliff:g id="appName" example="My app">%s</xliff:g>, it can add controls and content to this panel. In some apps, you can choose which controls show up here.</string>
+
<!-- a11y state description for a control that is currently favorited [CHAR LIMIT=NONE] -->
<string name="accessibility_control_favorite">Favorited</string>
<!-- a11y state description for a control that is currently favorited with its position [CHAR LIMIT=NONE] -->
@@ -2350,6 +2364,8 @@
<string name="controls_media_smartspace_rec_item_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
<!-- Description for Smartspace recommendation's media item which doesn't have artist info, including information for the media's title and the source app [CHAR LIMIT=NONE]-->
<string name="controls_media_smartspace_rec_item_no_artist_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%2$s</xliff:g></string>
+ <!-- Header title for Smartspace recommendation card within media controls. [CHAR_LIMIT=30] -->
+ <string name="controls_media_smartspace_rec_header">For You</string>
<!--- ****** Media tap-to-transfer ****** -->
<!-- Text for a button to undo the media transfer. [CHAR LIMIT=20] -->
@@ -2357,13 +2373,15 @@
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] -->
<string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to transfer media from the different device and back onto the current device. [CHAR LIMIT=75] -->
- <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string>
+ <string name="media_move_closer_to_end_cast">To play here, move closer to <xliff:g id="deviceName" example="tablet">%1$s</xliff:g></string>
<!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
<string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
- <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] -->
+ <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] -->
<string name="media_transfer_failed">Something went wrong. Try again.</string>
<!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] -->
<string name="media_transfer_loading">Loading</string>
+ <!-- Default name of the device. [CHAR LIMIT=30] -->
+ <string name="media_ttt_default_device_type">tablet</string>
<!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
<string name="controls_error_timeout">Inactive, check app</string>
@@ -2383,6 +2401,8 @@
<string name="controls_menu_add">Add controls</string>
<!-- Controls menu, edit [CHAR_LIMIT=30] -->
<string name="controls_menu_edit">Edit controls</string>
+ <!-- Controls menu, add another app [CHAR LIMIT=30] -->
+ <string name="controls_menu_add_another_app">Add app</string>
<!-- Title for the media output dialog with media related devices [CHAR LIMIT=50] -->
<string name="media_output_dialog_add_output">Add outputs</string>
@@ -2412,6 +2432,10 @@
<string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
<!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] -->
<string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string>
+ <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
+ <string name="media_output_group_title_suggested_device">Suggested Devices</string>
+ <!-- Sub status indicates device need premium account. [CHAR LIMIT=NONE] -->
+ <string name="media_output_status_require_premium">Requires premium account</string>
<!-- Media Output Broadcast Dialog -->
<!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
@@ -2766,6 +2790,39 @@
<!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] -->
<string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string>
+ <!-- Title for notification of low stylus battery with percentage. "percentage" is
+ the value of the battery capacity remaining [CHAR LIMIT=none]-->
+ <string name="stylus_battery_low_percentage"><xliff:g id="percentage" example="16%">%s</xliff:g> battery remaining</string>
+ <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]-->
+ <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string>
+
<!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] -->
<string name="stylus_battery_low">Stylus battery low</string>
+
+ <!-- Label for a lock screen shortcut to start the camera in video mode. [CHAR_LIMIT=16] -->
+ <string name="video_camera">Video camera</string>
+
+ <!-- Switch to work profile dialer app for placing a call dialog. -->
+ <!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work
+ profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] -->
+ <string name="call_from_work_profile_title">Can\'t call from this profile</string>
+ <!-- Text for switch to work profile for call dialog to guide users to make call from work
+ profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=NONE]
+ -->
+ <string name="call_from_work_profile_text">Your work policy allows you to make phone calls only from the work profile</string>
+ <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work
+ profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] -->
+ <string name="call_from_work_profile_action">Switch to work profile</string>
+ <!-- Label for the close button on switch to work profile dialog. Switch to work profile dialog guide users to make call from work
+ profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] -->
+ <string name="call_from_work_profile_close">Close</string>
+
+ <!--
+ Label for a menu item in a menu that is shown when the user wishes to configure the lock screen.
+ Clicking on this menu item takes the user to a screen where they can modify the settings of the
+ lock screen.
+
+ [CHAR LIMIT=32]
+ -->
+ <string name="lock_screen_settings">Lock screen settings</string>
</resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b11b6d6..58b0234 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -251,11 +251,12 @@
<style name="AuthCredentialPatternContainerStyle">
<item name="android:gravity">center</item>
- <item name="android:maxHeight">420dp</item>
- <item name="android:maxWidth">420dp</item>
- <item name="android:minHeight">200dp</item>
- <item name="android:minWidth">200dp</item>
- <item name="android:padding">20dp</item>
+ <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item>
+ <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item>
+ <item name="android:paddingHorizontal">32dp</item>
+ <item name="android:paddingVertical">20dp</item>
</style>
<style name="AuthCredentialPinPasswordContainerStyle">
@@ -677,6 +678,17 @@
<style name="MediaPlayer.Recommendation"/>
+ <style name="MediaPlayer.Recommendation.Header">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginTop">@dimen/qs_media_padding</item>
+ <item name="android:layout_marginStart">@dimen/qs_media_padding</item>
+ <item name="android:fontFamily">=@*android:string/config_headlineFontFamilyMedium</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
<style name="MediaPlayer.Recommendation.AlbumContainer">
<item name="android:layout_width">@dimen/qs_media_rec_album_size</item>
<item name="android:layout_height">@dimen/qs_media_rec_album_size</item>
@@ -685,6 +697,12 @@
<item name="android:layout_marginBottom">@dimen/qs_media_rec_album_bottom_margin</item>
</style>
+ <style name="MediaPlayer.Recommendation.AlbumContainer.Updated">
+ <item name="android:layout_width">@dimen/qs_media_rec_album_width</item>
+ <item name="android:background">@drawable/qs_media_light_source</item>
+ <item name="android:layout_marginTop">@dimen/qs_media_info_spacing</item>
+ </style>
+
<style name="MediaPlayer.Recommendation.Album">
<item name="android:backgroundTint">@color/media_player_album_bg</item>
</style>
diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml
index 06d425c..bf576dc 100644
--- a/packages/SystemUI/res/xml/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,105 +14,73 @@
~ limitations under the License.
-->
-<ConstraintSet
- xmlns:android="http://schemas.android.com/apk/res/android"
+<ConstraintSet xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/large_screen_header_constraint">
- <Constraint
- android:id="@+id/clock">
+ <Constraint android:id="@+id/clock">
<Layout
android:layout_width="wrap_content"
android:layout_height="0dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/date"
- app:layout_constraintHorizontal_bias="0"
- />
- <Transform
- android:scaleX="1"
- android:scaleY="1"
- />
+ app:layout_constraintStart_toEndOf="@id/begin_guide"
+ app:layout_constraintTop_toTopOf="parent" />
+ <PropertySet android:alpha="1" />
</Constraint>
- <Constraint
- android:id="@+id/date">
+ <Constraint android:id="@+id/date">
<Layout
android:layout_width="wrap_content"
android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/clock"
- app:layout_constraintEnd_toStartOf="@id/carrier_group"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="0"
- />
+ app:layout_constraintTop_toTopOf="parent" />
+ <PropertySet android:alpha="1" />
</Constraint>
- <Constraint
- android:id="@+id/carrier_group">
+ <Constraint android:id="@+id/carrier_group">
<Layout
- app:layout_constraintWidth_min="48dp"
android:layout_width="0dp"
android:layout_height="0dp"
- app:layout_constrainedWidth="true"
android:layout_gravity="end|center_vertical"
- android:layout_marginStart="8dp"
- app:layout_constraintStart_toEndOf="@id/date"
- app:layout_constraintEnd_toStartOf="@id/statusIcons"
- app:layout_constraintTop_toTopOf="@id/clock"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="1"
- />
- <PropertySet
- android:alpha="1"
- />
+ app:layout_constraintEnd_toStartOf="@id/statusIcons"
+ app:layout_constraintStart_toEndOf="@id/date"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_default="wrap"
+ app:layout_constraintWidth_min="48dp" />
+ <PropertySet android:alpha="1" />
</Constraint>
- <Constraint
- android:id="@+id/statusIcons">
+ <Constraint android:id="@+id/statusIcons">
<Layout
- app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
android:layout_width="wrap_content"
android:layout_height="@dimen/large_screen_shade_header_min_height"
- app:layout_constraintStart_toEndOf="@id/carrier_group"
- app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
- app:layout_constraintTop_toTopOf="@id/clock"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="1"
- />
- <PropertySet
- android:alpha="1"
- />
+ app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="@id/carrier_group"/>
+ <PropertySet android:alpha="1" />
</Constraint>
- <Constraint
- android:id="@+id/batteryRemainingIcon">
+ <Constraint android:id="@+id/batteryRemainingIcon">
<Layout
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
- app:layout_constraintStart_toEndOf="@id/statusIcons"
- app:layout_constraintEnd_toStartOf="@id/privacy_container"
- app:layout_constraintTop_toTopOf="@id/clock"
app:layout_constraintBottom_toBottomOf="parent"
- />
- <PropertySet
- android:alpha="1"
- />
+ app:layout_constraintEnd_toStartOf="@id/privacy_container"
+ app:layout_constraintTop_toTopOf="parent" />
+ <PropertySet android:alpha="1" />
</Constraint>
- <Constraint
- android:id="@+id/privacy_container">
+ <Constraint android:id="@+id/privacy_container">
<Layout
android:layout_width="wrap_content"
android:layout_height="@dimen/large_screen_shade_header_min_height"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="@id/date"
- app:layout_constraintBottom_toBottomOf="@id/date"
- app:layout_constraintStart_toEndOf="@id/batteryRemainingIcon"
- app:layout_constraintHorizontal_bias="1"
- />
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/end_guide"
+ app:layout_constraintTop_toTopOf="parent" />
</Constraint>
-
</ConstraintSet>
\ No newline at end of file
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml b/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml
new file mode 100644
index 0000000..d3be3c7
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<ConstraintSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+
+ <Constraint
+ android:id="@+id/sizing_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/qs_media_session_height_collapsed"
+ />
+
+ <Constraint
+ android:id="@+id/media_rec_title"
+ style="@style/MediaPlayer.Recommendation.Header"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <Constraint
+ android:id="@+id/media_cover1_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginStart="@dimen/qs_media_padding"
+ app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/>
+
+
+ <Constraint
+ android:id="@+id/media_cover2_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+ app:layout_constraintStart_toEndOf="@id/media_cover1_container"
+ app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/>
+
+ <Constraint
+ android:id="@+id/media_cover3_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
+ android:layout_marginEnd="@dimen/qs_media_padding"
+ app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+ app:layout_constraintStart_toEndOf="@id/media_cover2_container"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml b/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml
new file mode 100644
index 0000000..88c7055
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<ConstraintSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+
+ <Constraint
+ android:id="@+id/sizing_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/qs_media_session_height_expanded"
+ />
+
+ <Constraint
+ android:id="@+id/media_rec_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/qs_media_padding"
+ android:layout_marginStart="@dimen/qs_media_padding"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:singleLine="true"
+ android:textSize="14sp"
+ android:textColor="@color/notification_primary_text_color"/>
+
+ <Constraint
+ android:id="@+id/media_cover1_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ android:layout_height="@dimen/qs_media_rec_album_height_expanded"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginStart="@dimen/qs_media_padding"
+ app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/>
+
+
+ <Constraint
+ android:id="@+id/media_cover2_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ android:layout_height="@dimen/qs_media_rec_album_height_expanded"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+ app:layout_constraintStart_toEndOf="@id/media_cover1_container"
+ app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/>
+
+ <Constraint
+ android:id="@+id/media_cover3_container"
+ style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
+ android:layout_height="@dimen/qs_media_rec_album_height_expanded"
+ android:layout_marginEnd="@dimen/qs_media_padding"
+ app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
+ app:layout_constraintStart_toEndOf="@id/media_cover2_container"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index 1eb621e..d9c81af 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -66,6 +66,21 @@
app:layout_constraintTop_toBottomOf="@id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0" />
+
+ <Constraint
+ android:id="@+id/media_explicit_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginBottom="@dimen/qs_media_padding"
+ android:layout_marginTop="0dp"
+ app:layout_constraintStart_toStartOf="@id/header_title"
+ app:layout_constraintEnd_toStartOf="@id/header_artist"
+ app:layout_constraintTop_toTopOf="@id/header_artist"
+ app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintHorizontal_chainStyle="packed" />
+
<Constraint
android:id="@+id/header_artist"
android:layout_width="wrap_content"
@@ -75,9 +90,8 @@
app:layout_constraintEnd_toStartOf="@id/action_button_guideline"
app:layout_constrainedWidth="true"
app:layout_constraintTop_toBottomOf="@id/header_title"
- app:layout_constraintStart_toStartOf="@id/header_title"
- app:layout_constraintVertical_bias="0"
- app:layout_constraintHorizontal_bias="0" />
+ app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
+ app:layout_constraintVertical_bias="0" />
<Constraint
android:id="@+id/actionPlayPause"
diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml
index 7de0a5e..0cdc0f9 100644
--- a/packages/SystemUI/res/xml/media_session_expanded.xml
+++ b/packages/SystemUI/res/xml/media_session_expanded.xml
@@ -58,6 +58,21 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/header_artist"
app:layout_constraintHorizontal_bias="0" />
+
+ <Constraint
+ android:id="@+id/media_explicit_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/qs_media_info_spacing"
+ android:layout_marginBottom="@dimen/qs_media_padding"
+ android:layout_marginTop="0dp"
+ app:layout_constraintStart_toStartOf="@id/header_title"
+ app:layout_constraintEnd_toStartOf="@id/header_artist"
+ app:layout_constraintTop_toTopOf="@id/header_artist"
+ app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintHorizontal_chainStyle="packed"/>
+
<Constraint
android:id="@+id/header_artist"
android:layout_width="wrap_content"
@@ -67,10 +82,9 @@
android:layout_marginTop="0dp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/actionPlayPause"
- app:layout_constraintStart_toStartOf="@id/header_title"
+ app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
- app:layout_constraintVertical_bias="0"
- app:layout_constraintHorizontal_bias="0" />
+ app:layout_constraintVertical_bias="0" />
<Constraint
android:id="@+id/actionPlayPause"
diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml
index eca2b2a..52a98984 100644
--- a/packages/SystemUI/res/xml/qs_header.xml
+++ b/packages/SystemUI/res/xml/qs_header.xml
@@ -41,9 +41,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/privacy_container"
app:layout_constraintBottom_toBottomOf="@id/carrier_group"
- app:layout_constraintEnd_toStartOf="@id/carrier_group"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<Transform
android:scaleX="2.57"
@@ -56,28 +53,24 @@
<Layout
android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
- app:layout_constrainedWidth="true"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/space"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/carrier_group"
- app:layout_constraintHorizontal_bias="0"
- app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
</Constraint>
+ <!-- LargeScreenShadeHeaderController helps with managing clock width to layout this view -->
<Constraint
android:id="@+id/carrier_group">
<Layout
- app:layout_constraintWidth_min="48dp"
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="@dimen/large_screen_shade_header_min_height"
- app:layout_constraintStart_toEndOf="@id/clock"
+ app:layout_constraintWidth_min="48dp"
+ app:layout_constraintWidth_default="wrap"
+ app:layout_constraintStart_toStartOf="@id/clock"
app:layout_constraintTop_toBottomOf="@id/privacy_container"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="1"
app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon"
- app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<PropertySet
android:alpha="1"
@@ -87,39 +80,27 @@
<Constraint
android:id="@+id/statusIcons">
<Layout
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
- app:layout_constraintStart_toEndOf="@id/space"
+ app:layout_constraintWidth_default="wrap"
+ app:layout_constraintStart_toEndOf="@id/date"
app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
app:layout_constraintTop_toTopOf="@id/date"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintBottom_toBottomOf="@id/date"
/>
</Constraint>
<Constraint
android:id="@+id/batteryRemainingIcon">
<Layout
- android:layout_width="wrap_content"
+ android:layout_width="0dp"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+ app:layout_constraintWidth_default="wrap"
app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
- app:layout_constraintStart_toEndOf="@id/statusIcons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/date"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="1"
- app:layout_constraintHorizontal_chainStyle="spread_inside"
+ app:layout_constraintBottom_toBottomOf="@id/date"
/>
</Constraint>
-
- <Constraint
- android:id="@id/space">
- <Layout
- android:layout_width="0dp"
- android:layout_height="0dp"
- app:layout_constraintStart_toEndOf="@id/date"
- app:layout_constraintEnd_toStartOf="@id/statusIcons"
- />
- </Constraint>
</ConstraintSet>
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/.eslintrc.json b/packages/SystemUI/scripts/token_alignment/.eslintrc.json
new file mode 100644
index 0000000..69dc00e
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/.eslintrc.json
@@ -0,0 +1,31 @@
+{
+ "env": {
+ "es2021": true,
+ "node": true
+ },
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "plugins": ["prettier", "@typescript-eslint", "eslint-plugin-simple-import-sort", "import"],
+ "extends": ["prettier", "eslint:recommended", "plugin:@typescript-eslint/recommended"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ "argsIgnorePattern": "^_",
+ "varsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_"
+ }
+ ],
+ "no-multiple-empty-lines": ["error", { "max": 2 }],
+ "no-multi-spaces": "error",
+ "simple-import-sort/imports": "error",
+ "simple-import-sort/exports": "error",
+ "import/first": "error",
+ "import/newline-after-import": "error",
+ "import/no-duplicates": "error"
+ }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/.gitignore b/packages/SystemUI/scripts/token_alignment/.gitignore
new file mode 100644
index 0000000..96ce14f
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/.gitignore
@@ -0,0 +1,2 @@
+vscode
+node_modules
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/.prettierrc b/packages/SystemUI/scripts/token_alignment/.prettierrc
new file mode 100644
index 0000000..20f02f9
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "tabWidth": 4,
+ "printWidth": 100,
+ "semi": true,
+ "singleQuote": true,
+ "bracketSameLine": true,
+ "bracketSpacing": true,
+ "arrowParens": "always"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts b/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts
new file mode 100644
index 0000000..80e075c
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts
@@ -0,0 +1,297 @@
+// Copyright 2022 Google LLC
+
+// 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.
+
+type IElementComment =
+ | { commentNode: undefined; textContent: undefined; hidden: undefined }
+ | { commentNode: Node; textContent: string; hidden: boolean };
+
+interface ITag {
+ attrs?: Record<string, string | number>;
+ tagName: string;
+}
+
+export interface INewTag extends ITag {
+ content?: string | number;
+ comment?: string;
+}
+
+export type IUpdateTag = Partial<Omit<INewTag, 'tagName'>>;
+
+export default class DOM {
+ static addEntry(containerElement: Element, tagOptions: INewTag) {
+ const doc = containerElement.ownerDocument;
+ const exists = this.alreadyHasEntry(containerElement, tagOptions);
+
+ if (exists) {
+ console.log('Ignored adding entry already available: ', exists.outerHTML);
+ return;
+ }
+
+ let insertPoint: Node | null = containerElement.lastElementChild; //.childNodes[containerElement.childNodes.length - 1];
+
+ if (!insertPoint) {
+ console.log('Ignored adding entry in empity parent: ', containerElement.outerHTML);
+ return;
+ }
+
+ const { attrs, comment, content, tagName } = tagOptions;
+
+ if (comment) {
+ const commentNode = doc.createComment(comment);
+ this.insertAfterIdented(commentNode, insertPoint);
+ insertPoint = commentNode;
+ }
+
+ const newEl = doc.createElement(tagName);
+ if (content) newEl.innerHTML = content.toString();
+ if (attrs)
+ Object.entries(attrs).forEach(([attr, value]) =>
+ newEl.setAttribute(attr, value.toString())
+ );
+ this.insertAfterIdented(newEl, insertPoint);
+
+ return true;
+ }
+
+ static insertBeforeIndented(newNode: Node, referenceNode: Node) {
+ const paddingNode = referenceNode.previousSibling;
+ const ownerDoc = referenceNode.ownerDocument;
+ const containerNode = referenceNode.parentNode;
+
+ if (!paddingNode || !ownerDoc || !containerNode) return;
+
+ const currentPadding = paddingNode.textContent || '';
+ const textNode = referenceNode.ownerDocument.createTextNode(currentPadding);
+
+ containerNode.insertBefore(newNode, referenceNode);
+ containerNode.insertBefore(textNode, newNode);
+ }
+
+ static insertAfterIdented(newNode: Node, referenceNode: Node) {
+ const paddingNode = referenceNode.previousSibling;
+ const ownerDoc = referenceNode.ownerDocument;
+ const containerNode = referenceNode.parentNode;
+
+ if (!paddingNode || !ownerDoc || !containerNode) return;
+
+ const currentPadding = paddingNode.textContent || '';
+ const textNode = ownerDoc.createTextNode(currentPadding);
+
+ containerNode.insertBefore(newNode, referenceNode.nextSibling);
+ containerNode.insertBefore(textNode, newNode);
+ }
+
+ static getElementComment(el: Element): IElementComment {
+ const commentNode = el.previousSibling?.previousSibling;
+
+ const out = { commentNode: undefined, textContent: undefined, hidden: undefined };
+
+ if (!commentNode) return out;
+
+ const textContent = commentNode.textContent || '';
+ const hidden = textContent.substring(textContent.length - 6) == '@hide ';
+
+ if (!(commentNode && commentNode.nodeName == '#comment')) return out;
+
+ return { commentNode, textContent, hidden: hidden };
+ }
+
+ static duplicateEntryWithChange(
+ templateElement: Element,
+ options: Omit<IUpdateTag, 'content'>
+ ) {
+ const exists = this.futureEntryAlreadyExist(templateElement, options);
+ if (exists) {
+ console.log('Ignored duplicating entry already available: ', exists.outerHTML);
+ return;
+ }
+
+ const { commentNode } = this.getElementComment(templateElement);
+ let insertPoint: Node = templateElement;
+
+ if (commentNode) {
+ const newComment = commentNode.cloneNode();
+ this.insertAfterIdented(newComment, insertPoint);
+ insertPoint = newComment;
+ }
+
+ const newEl = templateElement.cloneNode(true) as Element;
+ this.insertAfterIdented(newEl, insertPoint);
+
+ this.updateElement(newEl, options);
+ return true;
+ }
+
+ static replaceStringInAttributeValueOnQueried(
+ root: Element,
+ query: string,
+ attrArray: string[],
+ replaceMap: Map<string, string>
+ ): boolean {
+ let updated = false;
+ const queried = [...Array.from(root.querySelectorAll(query)), root];
+
+ queried.forEach((el) => {
+ attrArray.forEach((attr) => {
+ if (el.hasAttribute(attr)) {
+ const currentAttrValue = el.getAttribute(attr);
+
+ if (!currentAttrValue) return;
+
+ [...replaceMap.entries()].some(([oldStr, newStr]) => {
+ if (
+ currentAttrValue.length >= oldStr.length &&
+ currentAttrValue.indexOf(oldStr) ==
+ currentAttrValue.length - oldStr.length
+ ) {
+ el.setAttribute(attr, currentAttrValue.replace(oldStr, newStr));
+ updated = true;
+ return true;
+ }
+ return false;
+ });
+ }
+ });
+ });
+
+ return updated;
+ }
+
+ static updateElement(el: Element, updateOptions: IUpdateTag) {
+ const exists = this.futureEntryAlreadyExist(el, updateOptions);
+ if (exists) {
+ console.log('Ignored updating entry already available: ', exists.outerHTML);
+ return;
+ }
+
+ const { comment, attrs, content } = updateOptions;
+
+ if (comment) {
+ const { commentNode } = this.getElementComment(el);
+ if (commentNode) {
+ commentNode.textContent = comment;
+ }
+ }
+
+ if (attrs) {
+ for (const attr in attrs) {
+ const value = attrs[attr];
+
+ if (value != undefined) {
+ el.setAttribute(attr, `${value}`);
+ } else {
+ el.removeAttribute(attr);
+ }
+ }
+ }
+
+ if (content != undefined) {
+ el.innerHTML = `${content}`;
+ }
+
+ return true;
+ }
+
+ static elementToOptions(el: Element): ITag {
+ return {
+ attrs: this.getAllElementAttributes(el),
+ tagName: el.tagName,
+ };
+ }
+
+ static getAllElementAttributes(el: Element): Record<string, string> {
+ return el
+ .getAttributeNames()
+ .reduce(
+ (acc, attr) => ({ ...acc, [attr]: el.getAttribute(attr) || '' }),
+ {} as Record<string, string>
+ );
+ }
+
+ static futureEntryAlreadyExist(el: Element, updateOptions: IUpdateTag) {
+ const currentElOptions = this.elementToOptions(el);
+
+ if (!el.parentElement) {
+ console.log('Checked el has no parent');
+ process.exit();
+ }
+
+ return this.alreadyHasEntry(el.parentElement, {
+ ...currentElOptions,
+ ...updateOptions,
+ attrs: { ...currentElOptions.attrs, ...updateOptions.attrs },
+ });
+ }
+
+ static alreadyHasEntry(
+ containerElement: Element,
+ { attrs, tagName }: Pick<INewTag, 'attrs' | 'tagName'>
+ ) {
+ const qAttrs = attrs
+ ? Object.entries(attrs)
+ .map(([a, v]) => `[${a}="${v}"]`)
+ .join('')
+ : '';
+
+ return containerElement.querySelector(tagName + qAttrs);
+ }
+
+ static replaceContentTextOnQueried(
+ root: Element,
+ query: string,
+ replacePairs: Array<[string, string]>
+ ) {
+ let updated = false;
+ let queried = Array.from(root.querySelectorAll(query));
+
+ if (queried.length == 0) queried = [...Array.from(root.querySelectorAll(query)), root];
+
+ queried.forEach((el) => {
+ replacePairs.forEach(([oldStr, newStr]) => {
+ if (el.innerHTML == oldStr) {
+ el.innerHTML = newStr;
+ updated = true;
+ }
+ });
+ });
+
+ return updated;
+ }
+
+ static XMLDocToString(doc: XMLDocument) {
+ let str = '';
+
+ doc.childNodes.forEach((node) => {
+ switch (node.nodeType) {
+ case 8: // comment
+ str += `<!--${node.nodeValue}-->\n`;
+ break;
+
+ case 3: // text
+ str += node.textContent;
+ break;
+
+ case 1: // element
+ str += (node as Element).outerHTML;
+ break;
+
+ default:
+ console.log('Unhandled node type: ' + node.nodeType);
+ break;
+ }
+ });
+
+ return str;
+ }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts b/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts
new file mode 100644
index 0000000..359e3ab
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts
@@ -0,0 +1,112 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import { exec } from 'child_process';
+import { parse } from 'csv-parse';
+import { promises as fs } from 'fs';
+import jsdom from 'jsdom';
+
+const DOMParser = new jsdom.JSDOM('').window.DOMParser as typeof window.DOMParser;
+
+type TFileList = string[];
+
+export type TCSVRecord = Array<string | boolean | number>;
+
+class _FileIO {
+ public parser = new DOMParser();
+ public saved: string[] = [];
+
+ public loadXML = async (path: string): Promise<XMLDocument> => {
+ try {
+ const src = await this.loadFileAsText(path);
+ return this.parser.parseFromString(src, 'text/xml') as XMLDocument;
+ } catch (error) {
+ console.log(`Failed to parse XML file '${path}'.`, error);
+ process.exit();
+ }
+ };
+
+ public loadFileAsText = async (path: string): Promise<string> => {
+ try {
+ return await fs.readFile(path, { encoding: 'utf8' });
+ } catch (error) {
+ console.log(`Failed to read file '${path}'.`, error);
+ process.exit();
+ }
+ };
+
+ public saveFile = async (data: string, path: string) => {
+ try {
+ await fs.writeFile(path, data, { encoding: 'utf8' });
+ this.saved.push(path);
+ } catch (error) {
+ console.log(error);
+ console.log(`Failed to write file '${path}'.`);
+ process.exit();
+ }
+ };
+
+ public loadFileList = async (path: string): Promise<TFileList> => {
+ const src = await this.loadFileAsText(path);
+
+ try {
+ return JSON.parse(src) as TFileList;
+ } catch (error) {
+ console.log(error);
+ console.log(`Failed to parse JSON file '${path}'.`);
+ process.exit();
+ }
+ };
+
+ public loadCSV = (path: string): Promise<Array<TCSVRecord>> => {
+ return new Promise((resolve, reject) => {
+ this.loadFileAsText(path).then((src) => {
+ parse(
+ src,
+ {
+ delimiter: ' ',
+ },
+ (err, records) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ resolve(records);
+ }
+ );
+ });
+ });
+ };
+
+ formatSaved = () => {
+ const cmd = `idea format ${this.saved.join(' ')}`;
+
+ exec(cmd, (error, out, stderr) => {
+ if (error) {
+ console.log(error.message);
+ return;
+ }
+
+ if (stderr) {
+ console.log(stderr);
+ return;
+ }
+
+ console.log(out);
+ });
+ };
+}
+
+export const FileIO = new _FileIO();
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts b/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts
new file mode 100644
index 0000000..8d50644
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts
@@ -0,0 +1,70 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import { FileIO, TCSVRecord } from './FileIO';
+import ProcessArgs from './processArgs';
+
+interface IInputMigItem {
+ migrationToken: string;
+ materialToken: string;
+ newDefaultValue?: string;
+ newComment?: string;
+}
+
+interface IAditionalKeys {
+ step: ('update' | 'duplicate' | 'add' | 'ignore')[];
+ isHidden: boolean;
+ replaceToken: string;
+}
+
+export type IMigItem = Omit<IInputMigItem, 'materialToken' | 'migrationToken'> & IAditionalKeys;
+
+export type IMigrationMap = Map<string, IMigItem>;
+
+function isMigrationRecord(record: TCSVRecord): record is string[] {
+ return !record.some((value) => typeof value != 'string') || record.length != 5;
+}
+
+export const loadMIgrationList = async function (): Promise<IMigrationMap> {
+ const out: IMigrationMap = new Map();
+ const csv = await FileIO.loadCSV('resources/migrationList.csv');
+
+ csv.forEach((record, i) => {
+ if (i == 0) return; // header
+
+ if (typeof record[0] != 'string') return;
+
+ if (!isMigrationRecord(record)) {
+ console.log(`Failed to validade CSV record as string[5].`, record);
+ process.exit();
+ }
+
+ const [originalToken, materialToken, newDefaultValue, newComment, migrationToken] = record;
+
+ if (out.has(originalToken)) {
+ console.log('Duplicated entry on Migration CSV file: ', originalToken);
+ return;
+ }
+
+ out.set(originalToken, {
+ replaceToken: ProcessArgs.isDebug ? migrationToken : materialToken,
+ ...(!!newDefaultValue && { newDefaultValue }),
+ ...(!!newComment && { newComment }),
+ step: [],
+ isHidden: false,
+ });
+ });
+
+ return new Map([...out].sort((a, b) => b[0].length - a[0].length));
+};
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts b/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts
new file mode 100644
index 0000000..be0e232
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts
@@ -0,0 +1,21 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+const myArgs = process.argv.slice(2);
+
+const ProcessArgs = {
+ isDebug: myArgs.includes('debug'),
+};
+
+export default ProcessArgs;
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts b/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts
new file mode 100644
index 0000000..368d4cb
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts
@@ -0,0 +1,102 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import DOM, { INewTag, IUpdateTag } from './DOMFuncs';
+import { FileIO } from './FileIO';
+import { IMigItem, IMigrationMap } from './migrationList';
+
+export type TResultExistingEval = ['update' | 'duplicate', IUpdateTag] | void;
+export type TResultMissingEval = INewTag | void;
+
+interface IProcessXML {
+ attr?: string;
+ containerQuery?: string;
+ evalExistingEntry?: TEvalExistingEntry;
+ evalMissingEntry?: TEvalMissingEntry;
+ hidable?: boolean;
+ path: string;
+ step: number;
+ tagName: string;
+}
+
+export type TEvalExistingEntry = (
+ attrname: string,
+ migItem: IMigItem,
+ qItem: Element
+) => TResultExistingEval;
+
+export type TEvalMissingEntry = (originalToken: string, migItem: IMigItem) => TResultMissingEval;
+
+export async function processQueriedEntries(
+ migrationMap: IMigrationMap,
+ {
+ attr = 'name',
+ containerQuery = '*',
+ evalExistingEntry,
+ path,
+ step,
+ tagName,
+ evalMissingEntry,
+ }: IProcessXML
+) {
+ const doc = await FileIO.loadXML(path);
+
+ const containerElement =
+ (containerQuery && doc.querySelector(containerQuery)) || doc.documentElement;
+
+ migrationMap.forEach((migItem, originalToken) => {
+ migItem.step[step] = 'ignore';
+
+ const queryTiems = containerElement.querySelectorAll(
+ `${tagName}[${attr}="${originalToken}"]`
+ );
+
+ if (evalMissingEntry) {
+ const addinOptions = evalMissingEntry(originalToken, migItem);
+
+ if (queryTiems.length == 0 && containerElement && addinOptions) {
+ DOM.addEntry(containerElement, addinOptions);
+ migItem.step[step] = 'add';
+ return;
+ }
+ }
+
+ if (evalExistingEntry)
+ queryTiems.forEach((qEl) => {
+ const attrName = qEl.getAttribute(attr);
+ const migItem = migrationMap.get(attrName || '');
+
+ if (!attrName || !migItem) return;
+
+ const updateOptions = evalExistingEntry(attrName, migItem, qEl);
+
+ if (!updateOptions) return;
+
+ const [processType, processOptions] = updateOptions;
+
+ switch (processType) {
+ case 'update':
+ if (DOM.updateElement(qEl, processOptions)) migItem.step[step] = 'update';
+ break;
+
+ case 'duplicate':
+ if (DOM.duplicateEntryWithChange(qEl, processOptions))
+ migItem.step[step] = 'duplicate';
+ break;
+ }
+ });
+ });
+
+ await FileIO.saveFile(doc.documentElement.outerHTML, path);
+}
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts b/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts
new file mode 100644
index 0000000..2c6f632
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts
@@ -0,0 +1,21 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+if (!process?.env?.ANDROID_BUILD_TOP) {
+ console.log(
+ "Error: Couldn't find 'ANDROID_BUILD_TOP' environment variable. Make sure to run 'lunch' in this terminal"
+ );
+}
+
+export const repoPath = process?.env?.ANDROID_BUILD_TOP;
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts b/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts
new file mode 100644
index 0000000..6679c5a
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts
@@ -0,0 +1,27 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+export function groupReplace(src: string, replaceMap: Map<string, string>, pattern: string) {
+ const fullPattern = pattern.replace('#group#', [...replaceMap.keys()].join('|'));
+
+ const regEx = new RegExp(fullPattern, 'g');
+
+ ''.replace;
+
+ return src.replace(regEx, (...args) => {
+ //match, ...matches, offset, string, groups
+ const [match, key] = args as string[];
+ return match.replace(key, replaceMap.get(key) || '');
+ });
+}
diff --git a/packages/SystemUI/scripts/token_alignment/index.ts b/packages/SystemUI/scripts/token_alignment/index.ts
new file mode 100644
index 0000000..1b15e48
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/index.ts
@@ -0,0 +1,240 @@
+// Copyright 2022 Google LLC
+
+// 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.import { exec } from 'child_process';
+
+import DOM from './helpers/DOMFuncs';
+import { FileIO } from './helpers/FileIO';
+import { loadMIgrationList } from './helpers/migrationList';
+import { processQueriedEntries, TEvalExistingEntry } from './helpers/processXML';
+import { repoPath } from './helpers/rootPath';
+import { groupReplace } from './helpers/textFuncs';
+
+async function init() {
+ const migrationMap = await loadMIgrationList();
+ const basePath = `${repoPath}/../tm-qpr-dev/frameworks/base/core/res/res/values/`;
+
+ await processQueriedEntries(migrationMap, {
+ containerQuery: 'declare-styleable[name="Theme"]',
+ hidable: true,
+ path: `${basePath}attrs.xml`,
+ step: 0,
+ tagName: 'attr',
+ evalExistingEntry: (_attrValue, migItem, qItem) => {
+ const { hidden, textContent: currentComment } = DOM.getElementComment(qItem);
+
+ if (hidden) migItem.isHidden = hidden;
+
+ const { newComment } = migItem;
+ return [
+ hidden ? 'update' : 'duplicate',
+ {
+ attrs: { name: migItem.replaceToken },
+ ...(newComment
+ ? { comment: `${newComment} @hide ` }
+ : currentComment
+ ? { comment: hidden ? currentComment : `${currentComment} @hide ` }
+ : {}),
+ },
+ ];
+ },
+ evalMissingEntry: (_originalToken, { replaceToken, newComment }) => {
+ return {
+ tagName: 'attr',
+ attrs: {
+ name: replaceToken,
+ format: 'color',
+ },
+ comment: `${newComment} @hide `,
+ };
+ },
+ });
+
+ // only update all existing entries
+ await processQueriedEntries(migrationMap, {
+ tagName: 'item',
+ path: `${basePath}themes_device_defaults.xml`,
+ containerQuery: 'resources',
+ step: 2,
+ evalExistingEntry: (_attrValue, { isHidden, replaceToken, step }, _qItem) => {
+ if (step[0] != 'ignore')
+ return [
+ isHidden ? 'update' : 'duplicate',
+ {
+ attrs: { name: replaceToken },
+ },
+ ];
+ },
+ });
+
+ // add missing entries on specific container
+ await processQueriedEntries(migrationMap, {
+ tagName: 'item',
+ path: `${basePath}themes_device_defaults.xml`,
+ containerQuery: 'resources style[parent="Theme.Material"]',
+ step: 3,
+ evalMissingEntry: (originalToken, { newDefaultValue, replaceToken }) => {
+ return {
+ tagName: 'item',
+ content: newDefaultValue,
+ attrs: {
+ name: replaceToken,
+ },
+ };
+ },
+ });
+
+ const evalExistingEntry: TEvalExistingEntry = (_attrValue, { replaceToken, step }, _qItem) => {
+ if (step[0] == 'update')
+ return [
+ 'update',
+ {
+ attrs: { name: replaceToken },
+ },
+ ];
+ };
+
+ await processQueriedEntries(migrationMap, {
+ tagName: 'item',
+ containerQuery: 'resources',
+ path: `${basePath}../values-night/themes_device_defaults.xml`,
+ step: 4,
+ evalExistingEntry,
+ });
+
+ await processQueriedEntries(migrationMap, {
+ tagName: 'java-symbol',
+ path: `${basePath}symbols.xml`,
+ containerQuery: 'resources',
+ step: 5,
+ evalExistingEntry,
+ });
+
+ // update attributes on tracked XML files
+ {
+ const searchAttrs = [
+ 'android:color',
+ 'android:indeterminateTint',
+ 'app:tint',
+ 'app:backgroundTint',
+ 'android:background',
+ 'android:tint',
+ 'android:drawableTint',
+ 'android:textColor',
+ 'android:fillColor',
+ 'android:startColor',
+ 'android:endColor',
+ 'name',
+ 'ns1:color',
+ ];
+
+ const filtered = new Map(
+ [...migrationMap]
+ .filter(([_originalToken, { step }]) => step[0] == 'update')
+ .map(([originalToken, { replaceToken }]) => [originalToken, replaceToken])
+ );
+
+ const query =
+ searchAttrs.map((str) => `*[${str}]`).join(',') +
+ [...filtered.keys()].map((originalToken) => `item[name*="${originalToken}"]`).join(',');
+
+ const trackedFiles = await FileIO.loadFileList(
+ `${__dirname}/resources/whitelist/xmls1.json`
+ );
+
+ const promises = trackedFiles.map(async (locaFilePath) => {
+ const filePath = `${repoPath}/${locaFilePath}`;
+
+ const doc = await FileIO.loadXML(filePath);
+ const docUpdated = DOM.replaceStringInAttributeValueOnQueried(
+ doc.documentElement,
+ query,
+ searchAttrs,
+ filtered
+ );
+ if (docUpdated) {
+ await FileIO.saveFile(DOM.XMLDocToString(doc), filePath);
+ } else {
+ console.warn(`Failed to update tracked file: '${locaFilePath}'`);
+ }
+ });
+ await Promise.all(promises);
+ }
+
+ // updates tag content on tracked files
+ {
+ const searchPrefixes = ['?android:attr/', '?androidprv:attr/'];
+ const filtered = searchPrefixes
+ .reduce<Array<[string, string]>>((acc, prefix) => {
+ return [
+ ...acc,
+ ...[...migrationMap.entries()]
+ .filter(([_originalToken, { step }]) => step[0] == 'update')
+ .map(
+ ([originalToken, { replaceToken }]) =>
+ [`${prefix}${originalToken}`, `${prefix}${replaceToken}`] as [
+ string,
+ string
+ ]
+ ),
+ ];
+ }, [])
+ .sort((a, b) => b[0].length - a[0].length);
+
+ const trackedFiles = await FileIO.loadFileList(
+ `${__dirname}/resources/whitelist/xmls2.json`
+ );
+
+ const promises = trackedFiles.map(async (locaFilePath) => {
+ const filePath = `${repoPath}/${locaFilePath}`;
+ const doc = await FileIO.loadXML(filePath);
+ const docUpdated = DOM.replaceContentTextOnQueried(
+ doc.documentElement,
+ 'item, color',
+ filtered
+ );
+ if (docUpdated) {
+ await FileIO.saveFile(DOM.XMLDocToString(doc), filePath);
+ } else {
+ console.warn(`Failed to update tracked file: '${locaFilePath}'`);
+ }
+ });
+ await Promise.all(promises);
+ }
+
+ // replace imports on Java / Kotlin
+ {
+ const replaceMap = new Map(
+ [...migrationMap.entries()]
+ .filter(([_originalToken, { step }]) => step[0] == 'update')
+ .map(
+ ([originalToken, { replaceToken }]) =>
+ [originalToken, replaceToken] as [string, string]
+ )
+ .sort((a, b) => b[0].length - a[0].length)
+ );
+
+ const trackedFiles = await FileIO.loadFileList(
+ `${__dirname}/resources/whitelist/java.json`
+ );
+
+ const promises = trackedFiles.map(async (locaFilePath) => {
+ const filePath = `${repoPath}/${locaFilePath}`;
+ const fileContent = await FileIO.loadFileAsText(filePath);
+ const str = groupReplace(fileContent, replaceMap, 'R.attr.(#group#)(?![a-zA-Z])');
+ await FileIO.saveFile(str, filePath);
+ });
+ await Promise.all(promises);
+ }
+}
+
+init();
diff --git a/packages/SystemUI/scripts/token_alignment/package-lock.json b/packages/SystemUI/scripts/token_alignment/package-lock.json
new file mode 100644
index 0000000..da9edb3
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/package-lock.json
@@ -0,0 +1,3356 @@
+{
+ "name": "token_alignment",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "csv-parse": "^5.3.3",
+ "high5": "^1.0.0",
+ "jsdom": "^20.0.3"
+ },
+ "devDependencies": {
+ "@types/jsdom": "^20.0.1",
+ "@types/node": "^18.11.18",
+ "@typescript-eslint/eslint-plugin": "^5.48.0",
+ "eslint-config-prettier": "^8.6.0",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-simple-import-sort": "^8.0.0",
+ "ts-node": "^10.9.1",
+ "typescript": "^4.9.4"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
+ "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+ "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+ "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
+ "dev": true
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
+ "dev": true
+ },
+ "node_modules/@types/jsdom": {
+ "version": "20.0.1",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
+ "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+ "dev": true
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "18.11.18",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
+ "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
+ "dev": true
+ },
+ "node_modules/@types/semver": {
+ "version": "7.3.13",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
+ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
+ "dev": true
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
+ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz",
+ "integrity": "sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.48.0",
+ "@typescript-eslint/type-utils": "5.48.0",
+ "@typescript-eslint/utils": "5.48.0",
+ "debug": "^4.3.4",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "regexpp": "^3.2.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.0.tgz",
+ "integrity": "sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.48.0",
+ "@typescript-eslint/types": "5.48.0",
+ "@typescript-eslint/typescript-estree": "5.48.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.0.tgz",
+ "integrity": "sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.48.0",
+ "@typescript-eslint/visitor-keys": "5.48.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.0.tgz",
+ "integrity": "sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.48.0",
+ "@typescript-eslint/utils": "5.48.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.0.tgz",
+ "integrity": "sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.0.tgz",
+ "integrity": "sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.48.0",
+ "@typescript-eslint/visitor-keys": "5.48.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.0.tgz",
+ "integrity": "sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.48.0",
+ "@typescript-eslint/types": "5.48.0",
+ "@typescript-eslint/typescript-estree": "5.48.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.0.tgz",
+ "integrity": "sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.48.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="
+ },
+ "node_modules/acorn": {
+ "version": "8.8.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
+ "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
+ "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
+ "dependencies": {
+ "acorn": "^8.1.0",
+ "acorn-walk": "^8.0.2"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peer": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
+ },
+ "node_modules/csv-parse": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.3.tgz",
+ "integrity": "sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw=="
+ },
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+ },
+ "node_modules/define-properties": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+ "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+ "dev": true,
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
+ "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.0.tgz",
+ "integrity": "sha512-GUGtW7eXQay0c+PRq0sGIKSdaBorfVqsCMhGHo4elP7YVqZu9nCZS4UkK4gv71gOWNMra/PaSKD3ao1oWExO0g==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.0",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.1.3",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.4",
+ "is-array-buffer": "^3.0.0",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.2",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+ "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.31.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz",
+ "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@eslint/eslintrc": "^1.4.1",
+ "@humanwhocodes/config-array": "^0.11.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.4.0",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-sdsl": "^4.1.4",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz",
+ "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
+ "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "resolve": "^1.20.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz",
+ "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.26.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
+ "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flat": "^1.2.5",
+ "debug": "^2.6.9",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-module-utils": "^2.7.3",
+ "has": "^1.0.3",
+ "is-core-module": "^2.8.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.5",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
+ "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+ "dev": true,
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.28.0",
+ "prettier": ">=2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-simple-import-sort": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz",
+ "integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==",
+ "dev": true,
+ "peerDependencies": {
+ "eslint": ">=5.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint/node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint/node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint/node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
+ "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
+ "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/fast-diff": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+ "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+ "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.19.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
+ "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/high5": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/high5/-/high5-1.0.0.tgz",
+ "integrity": "sha512-xucW/5M1hd+p6bj530wtRSKwqUQrgiIgOWepi4Di9abkonZaxhTDf0zrqqraxfZSXBcFSuc1/WVGBIlqSe1Hdw==",
+ "dependencies": {
+ "entities": "1.0"
+ }
+ },
+ "node_modules/high5/node_modules/entities": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
+ "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ=="
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz",
+ "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz",
+ "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+ "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+ "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/js-sdsl": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz",
+ "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==",
+ "dev": true,
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "20.0.3",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
+ "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+ "dependencies": {
+ "abab": "^2.0.6",
+ "acorn": "^8.8.1",
+ "acorn-globals": "^7.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.2",
+ "decimal.js": "^10.4.2",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.2",
+ "parse5": "^7.1.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.2",
+ "w3c-xmlserializer": "^4.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0",
+ "ws": "^8.11.0",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+ "dependencies": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
+ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz",
+ "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw=="
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+ "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dependencies": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz",
+ "integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
+ },
+ "node_modules/punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+ "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+ },
+ "node_modules/resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.3.8",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
+ "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+ "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+ "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
+ "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+ "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+ "dev": true,
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
+ "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.1",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+ "dependencies": {
+ "prelude-ls": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
+ "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
+ "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+ "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/ws": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/package.json b/packages/SystemUI/scripts/token_alignment/package.json
new file mode 100644
index 0000000..2e63668
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/package.json
@@ -0,0 +1,22 @@
+{
+ "dependencies": {
+ "csv-parse": "^5.3.3",
+ "high5": "^1.0.0",
+ "jsdom": "^20.0.3"
+ },
+ "devDependencies": {
+ "@types/jsdom": "^20.0.1",
+ "@types/node": "^18.11.18",
+ "@typescript-eslint/eslint-plugin": "^5.48.0",
+ "eslint-config-prettier": "^8.6.0",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-simple-import-sort": "^8.0.0",
+ "ts-node": "^10.9.1",
+ "typescript": "^4.9.4"
+ },
+ "scripts": {
+ "main": "ts-node ./index.ts",
+ "resetRepo": "repo forall -j100 -c 'git restore .'"
+ }
+}
diff --git a/packages/SystemUI/scripts/token_alignment/resources/migrationList.csv b/packages/SystemUI/scripts/token_alignment/resources/migrationList.csv
new file mode 100644
index 0000000..4111bb3
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/migrationList.csv
@@ -0,0 +1,16 @@
+android material newDefaultValue newComment migrationToken
+colorAccentPrimaryVariant colorPrimaryContainer MigTok02
+colorAccentSecondaryVariant colorSecondaryContainer MigTok04
+colorAccentTertiary colorTertiary MigTok05
+colorAccentTertiaryVariant colorTertiaryContainer MigTok06
+colorBackground colorSurfaceContainer MigTok07
+colorSurface colorSurfaceContainer MigTok08
+colorSurfaceHeader colorSurfaceContainerHighest MigTok09
+colorSurfaceHighlight colorSurfaceBright MigTok10
+colorSurfaceVariant colorSurfaceContainerHigh MigTok11
+textColorOnAccent colorOnPrimary MigTok12
+textColorPrimary colorOnSurface MigTok13
+textColorPrimaryInverse colorOnShadeInactive MigTok14
+textColorSecondary colorOnSurfaceVariant MigTok15
+textColorSecondaryInverse colorOnShadeInactiveVariant MigTok16
+textColorTertiary colorOutline MigTok17
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/resources/whitelist/java.json b/packages/SystemUI/scripts/token_alignment/resources/whitelist/java.json
new file mode 100644
index 0000000..7f55b2d
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/whitelist/java.json
@@ -0,0 +1,30 @@
+[
+ "frameworks/base/core/java/android/app/Notification.java",
+ "packages/apps/Settings/src/com/android/settings/dashboard/profileselector/UserAdapter.java",
+ "packages/apps/Settings/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java",
+ "frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt",
+ "frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt",
+ "packages/apps/WallpaperPicker2/src/com/android/wallpaper/picker/PreviewFragment.java",
+ "packages/apps/WallpaperPicker2/src/com/android/wallpaper/picker/CategorySelectorFragment.java",
+ "packages/apps/Launcher3/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt",
+ "frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java",
+ "vendor/unbundled_google/packages/NexusLauncher/src/com/google/android/apps/nexuslauncher/customize/WallpaperCarouselView.java",
+ "vendor/unbundled_google/packages/NexusLauncher/src/com/google/android/apps/nexuslauncher/quickstep/TaskOverlayFactoryImpl.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserViewBinder.kt",
+ "frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java",
+ "frameworks/base/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipView.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java",
+ "frameworks/base/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java"
+]
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls1.json b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls1.json
new file mode 100644
index 0000000..1e59773
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls1.json
@@ -0,0 +1,184 @@
+[
+ "frameworks/base/core/res/res/color/resolver_profile_tab_selected_bg.xml",
+ "frameworks/base/core/res/res/color-night/resolver_profile_tab_selected_bg.xml",
+ "frameworks/base/core/res/res/drawable/autofill_bottomsheet_background.xml",
+ "frameworks/base/core/res/res/drawable/btn_outlined.xml",
+ "frameworks/base/core/res/res/drawable/btn_tonal.xml",
+ "frameworks/base/core/res/res/drawable/chooser_action_button_bg.xml",
+ "frameworks/base/core/res/res/drawable/chooser_row_layer_list.xml",
+ "frameworks/base/core/res/res/drawable/resolver_outlined_button_bg.xml",
+ "frameworks/base/core/res/res/drawable/resolver_profile_tab_bg.xml",
+ "frameworks/base/core/res/res/drawable/toast_frame.xml",
+ "frameworks/base/core/res/res/drawable/work_widget_mask_view_background.xml",
+ "frameworks/base/core/res/res/layout/app_language_picker_current_locale_item.xml",
+ "frameworks/base/core/res/res/layout/app_language_picker_system_current.xml",
+ "frameworks/base/core/res/res/layout/autofill_save.xml",
+ "frameworks/base/core/res/res/layout/chooser_grid.xml",
+ "frameworks/base/core/res/res/layout/user_switching_dialog.xml",
+ "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/half_rounded_left_bk.xml",
+ "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/half_rounded_right_bk.xml",
+ "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/rounded_bk.xml",
+ "frameworks/base/packages/SettingsLib/ActionButtonsPreference/res/drawable/square_bk.xml",
+ "packages/modules/IntentResolver/java/res/drawable/chooser_action_button_bg.xml",
+ "packages/modules/IntentResolver/java/res/drawable/chooser_row_layer_list.xml",
+ "packages/modules/IntentResolver/java/res/drawable/resolver_outlined_button_bg.xml",
+ "packages/modules/IntentResolver/java/res/drawable/resolver_profile_tab_bg.xml",
+ "packages/modules/IntentResolver/java/res/layout/chooser_grid.xml",
+ "frameworks/base/libs/WindowManager/Shell/res/color/one_handed_tutorial_background_color.xml",
+ "frameworks/base/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_bg.xml",
+ "frameworks/base/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg.xml",
+ "frameworks/base/libs/WindowManager/Shell/res/drawable/bubble_stack_user_education_bg_rtl.xml",
+ "vendor/unbundled_google/packages/SystemUIGoogle/bcsmartspace/res/drawable/bg_smartspace_combination_sub_card.xml",
+ "vendor/unbundled_google/packages/SystemUIGoogle/bcsmartspace/res/layout/smartspace_combination_sub_card.xml",
+ "packages/apps/Launcher3/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml",
+ "packages/apps/Launcher3/res/drawable/rounded_action_button.xml",
+ "packages/apps/Launcher3/res/drawable/work_card.xml",
+ "packages/apps/Nfc/res/color/nfc_icon.xml",
+ "packages/apps/Nfc/res/color-night/nfc_icon.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/bg_overview_clear_all_button.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/bg_sandbox_feedback.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/bg_wellbeing_toast.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/button_taskbar_edu_bordered.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/button_taskbar_edu_colored.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/split_instructions_background.xml",
+ "packages/apps/Launcher3/quickstep/res/drawable/task_menu_item_bg.xml",
+ "packages/apps/Launcher3/quickstep/res/layout/digital_wellbeing_toast.xml",
+ "packages/apps/Launcher3/quickstep/res/layout/split_instructions_view.xml",
+ "packages/apps/Launcher3/quickstep/res/layout/taskbar_edu.xml",
+ "frameworks/base/packages/SettingsLib/res/drawable/broadcast_dialog_btn_bg.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color/share_target_text.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/all_apps_tab_background_selected.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/all_apps_tabs_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/arrow_tip_view_bg.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/button_bg.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/shortcut_halo.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-v31/surface.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-night-v31/all_apps_tab_background_selected.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/color-night-v31/surface.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/bg_pin_keyboard_snackbar_accept_button.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/bg_search_edu_preferences_button.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/circle_accentprimary_32dp.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/ic_search.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/ic_suggest_icon_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/share_target_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/sticky_snackbar_accept_btn_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/sticky_snackbar_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/sticky_snackbar_dismiss_btn_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/drawable/tall_card_btn_background.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/layout/section_header.xml",
+ "packages/apps/Settings/res/color/dream_card_color_state_list.xml",
+ "packages/apps/Settings/res/color/dream_card_icon_color_state_list.xml",
+ "packages/apps/Settings/res/color/dream_card_text_color_state_list.xml",
+ "packages/apps/Settings/res/drawable/accessibility_text_reading_preview.xml",
+ "packages/apps/Settings/res/drawable/broadcast_button_outline.xml",
+ "packages/apps/Settings/res/drawable/button_border_selected.xml",
+ "packages/apps/Settings/res/drawable/dream_preview_rounded_bg.xml",
+ "packages/apps/Settings/res/drawable/rounded_bg.xml",
+ "packages/apps/Settings/res/drawable/sim_confirm_dialog_btn_outline.xml",
+ "packages/apps/Settings/res/drawable/user_select_background.xml",
+ "packages/apps/Settings/res/drawable/volume_dialog_button_background_outline.xml",
+ "packages/apps/Settings/res/drawable/volume_dialog_button_background_solid.xml",
+ "packages/apps/Settings/res/layout/dream_preview_button.xml",
+ "packages/apps/Settings/res/layout/qrcode_scanner_fragment.xml",
+ "frameworks/base/packages/SystemUI/res/values-television/styles.xml",
+ "frameworks/base/packages/SystemUI/res/color/media_player_album_bg.xml",
+ "frameworks/base/packages/SystemUI/res/color/media_player_outline_button_bg.xml",
+ "frameworks/base/packages/SystemUI/res/color/media_player_solid_button_bg.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/color/numpad_key_color_secondary.xml",
+ "frameworks/base/packages/SystemUI/res/color/settingslib_state_on.xml",
+ "frameworks/base/packages/SystemUI/res/color/settingslib_track_off.xml",
+ "frameworks/base/packages/SystemUI/res/color/settingslib_track_on.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/accessibility_floating_tooltip_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/action_chip_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/action_chip_container_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/availability_dot_10dp.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_item_selected_bg.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_popup_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/brightness_progress_full_drawable.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/broadcast_dialog_btn_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/fgs_dot.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/fingerprint_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/ic_avatar_with_badge.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/drawable/kg_bouncer_secondary_button.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/drawable/kg_emergency_button_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/logout_button_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/media_ttt_chip_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/media_ttt_undo_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/notif_footer_btn_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/notification_guts_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/notification_material_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/overlay_badge_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/overlay_border.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/overlay_button_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/overlay_cancel.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/people_space_messages_count_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/people_tile_status_scrim.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/people_tile_suppressed_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/qs_media_outline_button.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/qs_media_solid_button.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/rounded_bg_full.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/screenrecord_button_background_solid.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/screenrecord_options_spinner_popup_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/screenrecord_spinner_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/screenshot_edit_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/user_switcher_fullscreen_button_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/volume_background_bottom.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/volume_background_top.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/volume_background_top_rounded.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/volume_row_rounded_background.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/volume_row_seekbar.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/wallet_action_button_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/wallet_app_button_bg.xml",
+ "frameworks/base/packages/SystemUI/res/drawable/wallet_empty_state_bg.xml",
+ "frameworks/base/packages/SystemUI/res/layout/alert_dialog_title_systemui.xml",
+ "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+ "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+ "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay.xml",
+ "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay.xml",
+ "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml",
+ "frameworks/base/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml",
+ "frameworks/base/packages/SystemUI/res/layout/internet_connectivity_dialog.xml",
+ "frameworks/base/packages/SystemUI/res/layout/notification_snooze.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_space_tile_view.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_large_with_content.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_medium_with_content.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml",
+ "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+ "frameworks/base/packages/SystemUI/res/layout/notification_snooze.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_small.xml",
+ "frameworks/base/packages/SystemUI/res/layout/people_tile_small_horizontal.xml",
+ "frameworks/base/packages/SystemUI/res/layout/screen_share_dialog.xml",
+ "frameworks/base/packages/SystemUI/res/layout/screen_share_dialog_spinner_item_text.xml",
+ "frameworks/base/packages/SystemUI/res/layout/user_switcher_fullscreen.xml",
+ "frameworks/base/packages/SystemUI/res/layout/user_switcher_fullscreen.xml",
+ "frameworks/base/packages/SystemUI/res/layout/wallet_empty_state.xml",
+ "frameworks/base/packages/SystemUI/res/layout/wallet_fullscreen.xml",
+ "frameworks/base/packages/SystemUI/res/layout/wallet_fullscreen.xml",
+ "frameworks/base/packages/SystemUI/res/layout/chipbar.xml",
+ "frameworks/base/packages/SystemUI/res/layout/notification_snooze.xml",
+ "vendor/unbundled_google/packages/SystemUIGoogle/res/drawable/columbus_chip_background_raw.xml",
+ "vendor/unbundled_google/packages/SystemUIGoogle/res/drawable/columbus_chip_background_raw.xml",
+ "vendor/unbundled_google/packages/SystemUIGoogle/res/drawable/columbus_dialog_background.xml",
+ "vendor/unbundled_google/packages/SystemUIGoogle/res/layout/columbus_target_request_dialog.xml",
+ "vendor/unbundled_google/packages/SettingsGoogle/res/color/dream_card_suw_color_state_list.xml",
+ "vendor/unbundled_google/packages/SettingsGoogle/res/drawable/dream_item_suw_rounded_bg.xml"
+]
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls2.json b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls2.json
new file mode 100644
index 0000000..20a7c76
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/resources/whitelist/xmls2.json
@@ -0,0 +1,13 @@
+[
+ "vendor/google/nexus_overlay/PixelDocumentsUIGoogleOverlay/res/values-v31/themes.xml",
+ "vendor/google/nexus_overlay/PixelDocumentsUIGoogleOverlay/res/values-night-v31/themes.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/values/colors.xml",
+ "vendor/unbundled_google/packages/NexusLauncher/res/values/styles.xml",
+ "packages/apps/Settings/res/values-night/colors.xml",
+ "packages/apps/Settings/res/values/colors.xml",
+ "packages/apps/Settings/res/values/styles.xml",
+ "frameworks/base/packages/SystemUI/res-keyguard/values/styles.xml",
+ "frameworks/base/packages/SystemUI/res/values/styles.xml",
+ "vendor/unbundled_google/packages/SettingsGoogle/res/values/styles.xml",
+ "vendor/unbundled_google/packages/SettingsGoogle/res/values/styles.xml"
+]
\ No newline at end of file
diff --git a/packages/SystemUI/scripts/token_alignment/tsconfig.json b/packages/SystemUI/scripts/token_alignment/tsconfig.json
new file mode 100644
index 0000000..20c7321
--- /dev/null
+++ b/packages/SystemUI/scripts/token_alignment/tsconfig.json
@@ -0,0 +1,103 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Speciffy the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+
+ /* Modules */
+ "module": "commonjs", /* Specify what module code is generated. */
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ "typeRoots": ["../node_modules/@types"], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ "resolveJsonModule": true, /* Enable importing .json files. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+ /* Emit */
+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
+
+ /* Type Checking */
+ "strict": true, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8a0fca0..28e786b 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -91,6 +91,9 @@
static_libs: [
"SystemUI-flag-types",
],
+ optimize: {
+ proguard_flags_files: ["proguard_flags.flags"],
+ },
java_version: "1.8",
min_sdk_version: "current",
}
diff --git a/packages/SystemUI/shared/proguard_flags.flags b/packages/SystemUI/shared/proguard_flags.flags
new file mode 100644
index 0000000..08859cd
--- /dev/null
+++ b/packages/SystemUI/shared/proguard_flags.flags
@@ -0,0 +1 @@
+-keep class * implements com.android.systemui.flags.ParcelableFlag
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
index 196f7f0..c9a25b0 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
@@ -47,10 +47,6 @@
val resourceId: Int
}
-interface DeviceConfigFlag<T> : Flag<T> {
- val default: T
-}
-
interface SysPropFlag<T> : Flag<T> {
val default: T
}
@@ -80,8 +76,8 @@
private constructor(parcel: Parcel) : this(
id = parcel.readInt(),
- name = parcel.readString(),
- namespace = parcel.readString(),
+ name = parcel.readString() ?: "",
+ namespace = parcel.readString() ?: "",
default = parcel.readBoolean(),
teamfood = parcel.readBoolean(),
overridden = parcel.readBoolean()
@@ -137,21 +133,6 @@
) : ResourceFlag<Boolean>
/**
- * A Flag that can reads its overrides from DeviceConfig.
- *
- * This is generally useful for flags that come from or are used _outside_ of SystemUI.
- *
- * Prefer [UnreleasedFlag] and [ReleasedFlag].
- */
-data class DeviceConfigBooleanFlag constructor(
- override val id: Int,
- override val name: String,
- override val namespace: String,
- override val default: Boolean = false,
- override val teamfood: Boolean = false
-) : DeviceConfigFlag<Boolean>
-
-/**
* A Flag that can reads its overrides from System Properties.
*
* This is generally useful for flags that come from or are used _outside_ of SystemUI.
@@ -186,8 +167,8 @@
private constructor(parcel: Parcel) : this(
id = parcel.readInt(),
- name = parcel.readString(),
- namespace = parcel.readString(),
+ name = parcel.readString() ?: "",
+ namespace = parcel.readString() ?: "",
default = parcel.readString() ?: ""
)
@@ -226,8 +207,8 @@
private constructor(parcel: Parcel) : this(
id = parcel.readInt(),
- name = parcel.readString(),
- namespace = parcel.readString(),
+ name = parcel.readString() ?: "",
+ namespace = parcel.readString() ?: "",
default = parcel.readInt()
)
@@ -266,8 +247,8 @@
private constructor(parcel: Parcel) : this(
id = parcel.readInt(),
- name = parcel.readString(),
- namespace = parcel.readString(),
+ name = parcel.readString() ?: "",
+ namespace = parcel.readString() ?: "",
default = parcel.readLong()
)
@@ -298,8 +279,8 @@
private constructor(parcel: Parcel) : this(
id = parcel.readInt(),
- name = parcel.readString(),
- namespace = parcel.readString(),
+ name = parcel.readString() ?: "",
+ namespace = parcel.readString() ?: "",
default = parcel.readFloat()
)
@@ -338,8 +319,8 @@
private constructor(parcel: Parcel) : this(
id = parcel.readInt(),
- name = parcel.readString(),
- namespace = parcel.readString(),
+ name = parcel.readString() ?: "",
+ namespace = parcel.readString() ?: "",
default = parcel.readDouble()
)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
index 195ba465..72a4fab 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
@@ -34,7 +34,7 @@
/** An event representing the change */
interface FlagEvent {
/** the id of the flag which changed */
- val flagId: Int
+ val flagName: String
/** if all listeners alerted invoke this method, the restart will be skipped */
fun requestNoRestart()
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index d85292a..da1641c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -39,7 +39,7 @@
const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
const val ACTION_SYSUI_STARTED = "com.android.systemui.STARTED"
- const val EXTRA_ID = "id"
+ const val EXTRA_NAME = "name"
const val EXTRA_VALUE = "value"
const val EXTRA_FLAGS = "flags"
private const val SETTINGS_PREFIX = "systemui/flags"
@@ -56,7 +56,7 @@
* that the restart be suppressed
*/
var onSettingsChangedAction: Consumer<Boolean>? = null
- var clearCacheAction: Consumer<Int>? = null
+ var clearCacheAction: Consumer<String>? = null
private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
private val settingsObserver: ContentObserver = SettingsObserver()
@@ -96,35 +96,42 @@
* Returns the stored value or null if not set.
* This API is used by TheFlippinApp.
*/
- fun isEnabled(id: Int): Boolean? = readFlagValue(id, BooleanFlagSerializer)
+ fun isEnabled(name: String): Boolean? = readFlagValue(name, BooleanFlagSerializer)
/**
* Sets the value of a boolean flag.
* This API is used by TheFlippinApp.
*/
- fun setFlagValue(id: Int, enabled: Boolean) {
- val intent = createIntent(id)
+ fun setFlagValue(name: String, enabled: Boolean) {
+ val intent = createIntent(name)
intent.putExtra(EXTRA_VALUE, enabled)
context.sendBroadcast(intent)
}
- fun eraseFlag(id: Int) {
- val intent = createIntent(id)
+ fun eraseFlag(name: String) {
+ val intent = createIntent(name)
context.sendBroadcast(intent)
}
/** Returns the stored value or null if not set. */
+ // TODO(b/265188950): Remove method this once ids are fully deprecated.
fun <T> readFlagValue(id: Int, serializer: FlagSerializer<T>): T? {
- val data = settings.getString(idToSettingsKey(id))
+ val data = settings.getStringFromSecure(idToSettingsKey(id))
+ return serializer.fromSettingsData(data)
+ }
+
+ /** Returns the stored value or null if not set. */
+ fun <T> readFlagValue(name: String, serializer: FlagSerializer<T>): T? {
+ val data = settings.getString(nameToSettingsKey(name))
return serializer.fromSettingsData(data)
}
override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
synchronized(listeners) {
val registerNeeded = listeners.isEmpty()
- listeners.add(PerFlagListener(flag.id, listener))
+ listeners.add(PerFlagListener(flag.name, listener))
if (registerNeeded) {
settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver)
}
@@ -143,38 +150,38 @@
}
}
- private fun createIntent(id: Int): Intent {
+ private fun createIntent(name: String): Intent {
val intent = Intent(ACTION_SET_FLAG)
intent.setPackage(RECEIVING_PACKAGE)
- intent.putExtra(EXTRA_ID, id)
+ intent.putExtra(EXTRA_NAME, name)
return intent
}
+ // TODO(b/265188950): Remove method this once ids are fully deprecated.
fun idToSettingsKey(id: Int): String {
return "$SETTINGS_PREFIX/$id"
}
+ fun nameToSettingsKey(name: String): String {
+ return "$SETTINGS_PREFIX/$name"
+ }
+
inner class SettingsObserver : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (uri == null) {
return
}
val parts = uri.pathSegments
- val idStr = parts[parts.size - 1]
- val id = try {
- idStr.toInt()
- } catch (e: NumberFormatException) {
- return
- }
- clearCacheAction?.accept(id)
- dispatchListenersAndMaybeRestart(id, onSettingsChangedAction)
+ val name = parts[parts.size - 1]
+ clearCacheAction?.accept(name)
+ dispatchListenersAndMaybeRestart(name, onSettingsChangedAction)
}
}
- fun dispatchListenersAndMaybeRestart(id: Int, restartAction: Consumer<Boolean>?) {
+ fun dispatchListenersAndMaybeRestart(name: String, restartAction: Consumer<Boolean>?) {
val filteredListeners: List<FlagListenable.Listener> = synchronized(listeners) {
- listeners.mapNotNull { if (it.id == id) it.listener else null }
+ listeners.mapNotNull { if (it.name == name) it.listener else null }
}
// If there are no listeners, there's nothing to dispatch to, and nothing to suppress it.
if (filteredListeners.isEmpty()) {
@@ -185,7 +192,7 @@
val suppressRestartList: List<Boolean> = filteredListeners.map { listener ->
var didRequestNoRestart = false
val event = object : FlagListenable.FlagEvent {
- override val flagId = id
+ override val flagName = name
override fun requestNoRestart() {
didRequestNoRestart = true
}
@@ -198,7 +205,7 @@
restartAction?.accept(suppressRestart)
}
- private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
+ private data class PerFlagListener(val name: String, val listener: FlagListenable.Listener)
}
class NoFlagResultsException : Exception(
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt
index 742bb0b..6beb851 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt
@@ -22,7 +22,10 @@
class FlagSettingsHelper(private val contentResolver: ContentResolver) {
- fun getString(key: String): String? = Settings.Secure.getString(contentResolver, key)
+ // TODO(b/265188950): Remove method this once ids are fully deprecated.
+ fun getStringFromSecure(key: String): String? = Settings.Secure.getString(contentResolver, key)
+
+ fun getString(key: String): String? = Settings.Global.getString(contentResolver, key)
fun registerContentObserver(
name: String,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt
new file mode 100644
index 0000000..a9a5cf9
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputDevice.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.shared.hardware
+
+import android.view.InputDevice
+
+/**
+ * Returns true if [InputDevice] is electronic components to allow a user to use an active stylus in
+ * the host device or a passive stylus is detected by the host device.
+ */
+val InputDevice.isInternalStylusSource: Boolean
+ get() = isAnyStylusSource && !isExternal
+
+/** Returns true if [InputDevice] is an active stylus. */
+val InputDevice.isExternalStylusSource: Boolean
+ get() = isAnyStylusSource && isExternal
+
+/**
+ * Returns true if [InputDevice] supports any stylus source.
+ *
+ * @see InputDevice.isInternalStylusSource
+ * @see InputDevice.isExternalStylusSource
+ */
+val InputDevice.isAnyStylusSource: Boolean
+ get() = supportsSource(InputDevice.SOURCE_STYLUS)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt
new file mode 100644
index 0000000..f020b4e
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/hardware/InputManager.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.shared.hardware
+
+import android.hardware.input.InputManager
+import android.view.InputDevice
+
+/**
+ * Gets information about all input devices in the system and returns as a lazy [Sequence].
+ *
+ * For performance reasons, it is preferred to operate atop the returned [Sequence] to ensure each
+ * operation is executed on an element-per-element basis yet customizable.
+ *
+ * For example:
+ * ```kotlin
+ * val stylusDevices = inputManager.getInputDeviceSequence().filter {
+ * it.supportsSource(InputDevice.SOURCE_STYLUS)
+ * }
+ *
+ * val hasInternalStylus = stylusDevices.any { it.isInternal }
+ * val hasExternalStylus = stylusDevices.any { !it.isInternal }
+ * ```
+ *
+ * @return a [Sequence] of [InputDevice].
+ */
+fun InputManager.getInputDeviceSequence(): Sequence<InputDevice> =
+ inputDeviceIds.asSequence().mapNotNull { getInputDevice(it) }
+
+/**
+ * Returns the first [InputDevice] matching the given predicate, or null if no such [InputDevice]
+ * was found.
+ */
+fun InputManager.findInputDevice(predicate: (InputDevice) -> Boolean): InputDevice? =
+ getInputDeviceSequence().find { predicate(it) }
+
+/**
+ * Returns true if [any] [InputDevice] matches with [predicate].
+ *
+ * For example:
+ * ```kotlin
+ * val hasStylusSupport = inputManager.hasInputDevice { it.isStylusSupport() }
+ * val hasStylusPen = inputManager.hasInputDevice { it.isStylusPen() }
+ * ```
+ */
+fun InputManager.hasInputDevice(predicate: (InputDevice) -> Boolean): Boolean =
+ getInputDeviceSequence().any { predicate(it) }
+
+/** Returns true if host device has any [InputDevice] where [InputDevice.isInternalStylusSource]. */
+fun InputManager.hasInternalStylusSource(): Boolean = hasInputDevice { it.isInternalStylusSource }
+
+/** Returns true if host device has any [InputDevice] where [InputDevice.isExternalStylusSource]. */
+fun InputManager.hasExternalStylusSource(): Boolean = hasInputDevice { it.isExternalStylusSource }
+
+/** Returns true if host device has any [InputDevice] where [InputDevice.isAnyStylusSource]. */
+fun InputManager.hasAnyStylusSource(): Boolean = hasInputDevice { it.isAnyStylusSource }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 1c532fe..b8bddd1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -22,6 +22,7 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.view.MotionEvent;
+import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.shared.recents.model.Task;
@@ -87,12 +88,6 @@
void notifyPrioritizedRotation(int rotation) = 25;
/**
- * Handle the provided image as if it was a screenshot.
- */
- void handleImageBundleAsScreenshot(in Bundle screenImageBundle, in Rect locationInScreen,
- in Insets visibleInsets, in Task.TaskKey task) = 28;
-
- /**
* Notifies to expand notification panel.
*/
void expandNotificationPanel() = 29;
@@ -125,5 +120,10 @@
*/
void toggleNotificationPanel() = 50;
- // Next id = 51
+ /**
+ * Handle the screenshot request.
+ */
+ void takeScreenshot(in ScreenshotRequest request) = 51;
+
+ // Next id = 52
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
index 0ee813b..ef2247f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
@@ -15,39 +15,36 @@
*/
package com.android.systemui.shared.regionsampling
+import android.app.WallpaperColors
+import android.app.WallpaperManager
import android.graphics.Color
+import android.graphics.Point
import android.graphics.Rect
+import android.graphics.RectF
import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.systemui.shared.navigationbar.RegionSamplingHelper
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCallback
import java.io.PrintWriter
import java.util.concurrent.Executor
/** Class for instance of RegionSamplingHelper */
-open class RegionSampler(
- sampledView: View?,
+open class RegionSampler
+@JvmOverloads
+constructor(
+ val sampledView: View?,
mainExecutor: Executor?,
- bgExecutor: Executor?,
- regionSamplingEnabled: Boolean,
- updateFun: UpdateColorCallback
-) {
+ val bgExecutor: Executor?,
+ val regionSamplingEnabled: Boolean,
+ val updateForegroundColor: UpdateColorCallback,
+ val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView?.context)
+) : WallpaperManager.LocalWallpaperColorConsumer {
private var regionDarkness = RegionDarkness.DEFAULT
private var samplingBounds = Rect()
private val tmpScreenLocation = IntArray(2)
@VisibleForTesting var regionSampler: RegionSamplingHelper? = null
private var lightForegroundColor = Color.WHITE
private var darkForegroundColor = Color.BLACK
-
- @VisibleForTesting
- open fun createRegionSamplingHelper(
- sampledView: View,
- callback: SamplingCallback,
- mainExecutor: Executor?,
- bgExecutor: Executor?
- ): RegionSamplingHelper {
- return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
- }
+ private val displaySize = Point()
/**
* Sets the colors to be used for Dark and Light Foreground.
@@ -73,7 +70,7 @@
}
}
- private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
+ private fun getRegionDarkness(isRegionDark: Boolean): RegionDarkness {
return if (isRegionDark) {
RegionDarkness.DARK
} else {
@@ -87,12 +84,32 @@
/** Start region sampler */
fun startRegionSampler() {
- regionSampler?.start(samplingBounds)
+ if (!regionSamplingEnabled || sampledView == null) {
+ return
+ }
+
+ val sampledRegion = calculateSampledRegion(sampledView)
+ val regions = ArrayList<RectF>()
+ val sampledRegionWithOffset = convertBounds(sampledRegion)
+ regions.add(sampledRegionWithOffset)
+
+ wallpaperManager?.removeOnColorsChangedListener(this)
+ wallpaperManager?.addOnColorsChangedListener(this, regions)
+
+ // TODO(b/265969235): conditionally set FLAG_LOCK or FLAG_SYSTEM once HS smartspace
+ // implemented
+ bgExecutor?.execute(
+ Runnable {
+ val initialSampling =
+ wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)
+ onColorsChanged(sampledRegionWithOffset, initialSampling)
+ }
+ )
}
/** Stop region sampler */
fun stopRegionSampler() {
- regionSampler?.stop()
+ wallpaperManager?.removeOnColorsChangedListener(this)
}
/** Dump region sampler */
@@ -100,43 +117,66 @@
regionSampler?.dump(pw)
}
- init {
- if (regionSamplingEnabled && sampledView != null) {
- regionSampler =
- createRegionSamplingHelper(
- sampledView,
- object : SamplingCallback {
- override fun onRegionDarknessChanged(isRegionDark: Boolean) {
- regionDarkness = convertToClockDarkness(isRegionDark)
- updateFun()
- }
- /**
- * The method getLocationOnScreen is used to obtain the view coordinates
- * relative to its left and top edges on the device screen. Directly
- * accessing the X and Y coordinates of the view returns the location
- * relative to its parent view instead.
- */
- override fun getSampledRegion(sampledView: View): Rect {
- val screenLocation = tmpScreenLocation
- sampledView.getLocationOnScreen(screenLocation)
- val left = screenLocation[0]
- val top = screenLocation[1]
- samplingBounds.left = left
- samplingBounds.top = top
- samplingBounds.right = left + sampledView.width
- samplingBounds.bottom = top + sampledView.height
- return samplingBounds
- }
+ fun calculateSampledRegion(sampledView: View): RectF {
+ val screenLocation = tmpScreenLocation
+ /**
+ * The method getLocationOnScreen is used to obtain the view coordinates relative to its
+ * left and top edges on the device screen. Directly accessing the X and Y coordinates of
+ * the view returns the location relative to its parent view instead.
+ */
+ sampledView.getLocationOnScreen(screenLocation)
+ val left = screenLocation[0]
+ val top = screenLocation[1]
- override fun isSamplingEnabled(): Boolean {
- return regionSamplingEnabled
- }
- },
- mainExecutor,
- bgExecutor
- )
- }
- regionSampler?.setWindowVisible(true)
+ samplingBounds.left = left
+ samplingBounds.top = top
+ samplingBounds.right = left + sampledView.width
+ samplingBounds.bottom = top + sampledView.height
+
+ return RectF(samplingBounds)
+ }
+
+ /**
+ * Convert the bounds of the region we want to sample from to fractional offsets because
+ * WallpaperManager requires the bounds to be between [0,1]. The wallpaper is treated as one
+ * continuous image, so if there are multiple screens, then each screen falls into a fractional
+ * range. For instance, 4 screens have the ranges [0, 0.25], [0,25, 0.5], [0.5, 0.75], [0.75,
+ * 1].
+ */
+ fun convertBounds(originalBounds: RectF): RectF {
+
+ // TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER
+ // TODO(b/265968912): remove hard-coded value once LS wallpaper supported
+ val wallpaperPageNum = 0
+ val numScreens = 1
+
+ val screenWidth = displaySize.x
+ // TODO: investigate small difference between this and the height reported in go/web-hv
+ val screenHeight = displaySize.y
+
+ val newBounds = RectF()
+ // horizontal
+ newBounds.left = ((originalBounds.left / screenWidth) + wallpaperPageNum) / numScreens
+ newBounds.right = ((originalBounds.right / screenWidth) + wallpaperPageNum) / numScreens
+ // vertical
+ newBounds.top = originalBounds.top / screenHeight
+ newBounds.bottom = originalBounds.bottom / screenHeight
+
+ return newBounds
+ }
+
+ init {
+ sampledView?.context?.display?.getSize(displaySize)
+ }
+
+ override fun onColorsChanged(area: RectF?, colors: WallpaperColors?) {
+ // update text color when wallpaper color changes
+ regionDarkness =
+ getRegionDarkness(
+ (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) !=
+ WallpaperColors.HINT_SUPPORTS_DARK_TEXT
+ )
+ updateForegroundColor()
}
}
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 857cc462..5d036fb 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
@@ -35,6 +35,7 @@
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
+import androidx.annotation.BoolRes;
import androidx.core.view.OneShotPreDrawListener;
import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
@@ -65,6 +66,8 @@
private final int mTaskbarBottomMarginResource;
@DimenRes
private final int mButtonDiameterResource;
+ @BoolRes
+ private final int mFloatingRotationBtnPositionLeftResource;
private AnimatedVectorDrawable mAnimatedDrawable;
private boolean mIsShowing;
@@ -84,7 +87,7 @@
@LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin,
@DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin,
@DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
- @DimenRes int rippleMaxWidth) {
+ @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
mWindowManager = context.getSystemService(WindowManager.class);
mKeyButtonContainer = (ViewGroup) LayoutInflater.from(context).inflate(layout, null);
mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
@@ -100,6 +103,7 @@
mTaskbarLeftMarginResource = taskbarLeftMargin;
mTaskbarBottomMarginResource = taskbarBottomMargin;
mButtonDiameterResource = buttonDiameter;
+ mFloatingRotationBtnPositionLeftResource = floatingRotationBtnPositionLeftResource;
updateDimensionResources();
}
@@ -116,8 +120,11 @@
int taskbarMarginBottom =
res.getDimensionPixelSize(mTaskbarBottomMarginResource);
+ boolean floatingRotationButtonPositionLeft =
+ res.getBoolean(mFloatingRotationBtnPositionLeftResource);
+
mPositionCalculator = new FloatingRotationButtonPositionCalculator(defaultMargin,
- taskbarMarginLeft, taskbarMarginBottom);
+ taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft);
final int diameter = res.getDimensionPixelSize(mButtonDiameterResource);
mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
index ec3c073..40e43a9 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
@@ -10,7 +10,8 @@
class FloatingRotationButtonPositionCalculator(
private val defaultMargin: Int,
private val taskbarMarginLeft: Int,
- private val taskbarMarginBottom: Int
+ private val taskbarMarginBottom: Int,
+ private val floatingRotationButtonPositionLeft: Boolean
) {
fun calculatePosition(
@@ -18,7 +19,6 @@
taskbarVisible: Boolean,
taskbarStashed: Boolean
): Position {
-
val isTaskbarSide = currentRotation == Surface.ROTATION_0
|| currentRotation == Surface.ROTATION_90
val useTaskbarMargin = isTaskbarSide && taskbarVisible && !taskbarStashed
@@ -55,11 +55,21 @@
)
private fun resolveGravity(rotation: Int): Int =
- when (rotation) {
- Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.LEFT
- Surface.ROTATION_90 -> Gravity.BOTTOM or Gravity.RIGHT
- Surface.ROTATION_180 -> Gravity.TOP or Gravity.RIGHT
- Surface.ROTATION_270 -> Gravity.TOP or Gravity.LEFT
- else -> throw IllegalArgumentException("Invalid rotation $rotation")
+ if (floatingRotationButtonPositionLeft) {
+ when (rotation) {
+ Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.LEFT
+ Surface.ROTATION_90 -> Gravity.BOTTOM or Gravity.RIGHT
+ Surface.ROTATION_180 -> Gravity.TOP or Gravity.RIGHT
+ Surface.ROTATION_270 -> Gravity.TOP or Gravity.LEFT
+ else -> throw IllegalArgumentException("Invalid rotation $rotation")
+ }
+ } else {
+ when (rotation) {
+ Surface.ROTATION_0 -> Gravity.BOTTOM or Gravity.RIGHT
+ Surface.ROTATION_90 -> Gravity.TOP or Gravity.RIGHT
+ Surface.ROTATION_180 -> Gravity.TOP or Gravity.LEFT
+ Surface.ROTATION_270 -> Gravity.BOTTOM or Gravity.LEFT
+ else -> throw IllegalArgumentException("Invalid rotation $rotation")
+ }
}
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
index 25d2721..9b73cc3 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
@@ -48,48 +48,28 @@
val drawableInsetSize: Int
try {
val keyShadowBlur =
- attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0)
+ attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f)
val keyShadowOffsetX =
- attributes.getDimensionPixelSize(
- R.styleable.DoubleShadowTextView_keyShadowOffsetX,
- 0
- )
+ attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f)
val keyShadowOffsetY =
- attributes.getDimensionPixelSize(
- R.styleable.DoubleShadowTextView_keyShadowOffsetY,
- 0
- )
+ attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f)
val keyShadowAlpha =
attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f)
mKeyShadowInfo =
- ShadowInfo(
- keyShadowBlur.toFloat(),
- keyShadowOffsetX.toFloat(),
- keyShadowOffsetY.toFloat(),
- keyShadowAlpha
- )
+ ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha)
val ambientShadowBlur =
- attributes.getDimensionPixelSize(
- R.styleable.DoubleShadowTextView_ambientShadowBlur,
- 0
- )
+ attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f)
val ambientShadowOffsetX =
- attributes.getDimensionPixelSize(
- R.styleable.DoubleShadowTextView_ambientShadowOffsetX,
- 0
- )
+ attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f)
val ambientShadowOffsetY =
- attributes.getDimensionPixelSize(
- R.styleable.DoubleShadowTextView_ambientShadowOffsetY,
- 0
- )
+ attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f)
val ambientShadowAlpha =
attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f)
mAmbientShadowInfo =
ShadowInfo(
- ambientShadowBlur.toFloat(),
- ambientShadowOffsetX.toFloat(),
- ambientShadowOffsetY.toFloat(),
+ ambientShadowBlur,
+ ambientShadowOffsetX,
+ ambientShadowOffsetY,
ambientShadowAlpha
)
drawableSize =
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index fd41cb06..6bfaf5e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -283,17 +283,6 @@
}
/**
- * @return whether screen pinning is active.
- */
- public boolean isScreenPinningActive() {
- try {
- return getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED;
- } catch (RemoteException e) {
- return false;
- }
- }
-
- /**
* @return whether screen pinning is enabled.
*/
public boolean isScreenPinningEnabled() {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 766266d..037a71e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -112,7 +112,8 @@
public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
// Freeform windows are showing in desktop mode
public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
-
+ // Device dreaming state
+ public static final int SYSUI_STATE_DEVICE_DREAMING = 1 << 27;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -141,7 +142,8 @@
SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
SYSUI_STATE_IMMERSIVE_MODE,
SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
- SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE
+ SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
+ SYSUI_STATE_DEVICE_DREAMING
})
public @interface SystemUiStateFlags {}
@@ -179,6 +181,7 @@
str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : "");
str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0
? "freeform_active_in_desktop_mode" : "");
+ str.add((flags & SYSUI_STATE_DEVICE_DREAMING) != 0 ? "device_dreaming" : "");
return str.toString();
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index 8af934f..dd52cfb 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -16,6 +16,7 @@
package com.android.systemui.shared.system;
+import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.app.TaskStackListener;
@@ -27,6 +28,8 @@
import android.util.Log;
import android.window.TaskSnapshot;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.os.SomeArgs;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -43,15 +46,51 @@
private final Impl mImpl;
+ /**
+ * Proxies calls to the given handler callback synchronously for testing purposes.
+ */
+ private static class TestSyncHandler extends Handler {
+ private Handler.Callback mCb;
+
+ public TestSyncHandler() {
+ super(Looper.getMainLooper());
+ }
+
+ public void setCallback(Handler.Callback cb) {
+ mCb = cb;
+ }
+
+ @Override
+ public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
+ return mCb.handleMessage(msg);
+ }
+ }
+
private TaskStackChangeListeners() {
mImpl = new Impl(Looper.getMainLooper());
}
+ private TaskStackChangeListeners(Handler h) {
+ mImpl = new Impl(h);
+ }
+
public static TaskStackChangeListeners getInstance() {
return INSTANCE;
}
/**
+ * Returns an instance of the listeners that can be called upon synchronously for testsing
+ * purposes.
+ */
+ @VisibleForTesting
+ public static TaskStackChangeListeners getTestInstance() {
+ TestSyncHandler h = new TestSyncHandler();
+ TaskStackChangeListeners l = new TaskStackChangeListeners(h);
+ h.setCallback(l.mImpl);
+ return l;
+ }
+
+ /**
* Registers a task stack listener with the system.
* This should be called on the main thread.
*/
@@ -71,7 +110,15 @@
}
}
- private static class Impl extends TaskStackListener implements Handler.Callback {
+ /**
+ * Returns an instance of the listener to call upon from tests.
+ */
+ @VisibleForTesting
+ public TaskStackListener getListenerImpl() {
+ return mImpl;
+ }
+
+ private class Impl extends TaskStackListener implements Handler.Callback {
private static final int ON_TASK_STACK_CHANGED = 1;
private static final int ON_TASK_SNAPSHOT_CHANGED = 2;
@@ -104,10 +151,14 @@
private final Handler mHandler;
private boolean mRegistered;
- Impl(Looper looper) {
+ private Impl(Looper looper) {
mHandler = new Handler(looper, this);
}
+ private Impl(Handler handler) {
+ mHandler = handler;
+ }
+
public void addListener(TaskStackChangeListener listener) {
synchronized (mTaskStackListeners) {
mTaskStackListeners.add(listener);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt
index 7f2933e..c9e57b4 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt
@@ -15,21 +15,51 @@
package com.android.systemui.unfold.system
import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration
+import android.os.Trace
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class ActivityManagerActivityTypeProvider @Inject constructor(
- private val activityManager: ActivityManager
-) : CurrentActivityTypeProvider {
+class ActivityManagerActivityTypeProvider
+@Inject
+constructor(private val activityManager: ActivityManager) : CurrentActivityTypeProvider {
override val isHomeActivity: Boolean?
- get() {
- val activityType = activityManager.getRunningTasks(/* maxNum= */ 1)
- ?.getOrNull(0)?.topActivityType ?: return null
+ get() = _isHomeActivity
- return activityType == WindowConfiguration.ACTIVITY_TYPE_HOME
+ private var _isHomeActivity: Boolean? = null
+
+
+ override fun init() {
+ _isHomeActivity = activityManager.isOnHomeActivity()
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
+ }
+
+ override fun uninit() {
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+ }
+
+ private val taskStackChangeListener =
+ object : TaskStackChangeListener {
+ override fun onTaskMovedToFront(taskInfo: RunningTaskInfo) {
+ _isHomeActivity = taskInfo.isHomeActivity()
+ }
}
+
+ private fun RunningTaskInfo.isHomeActivity(): Boolean =
+ topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME
+
+ private fun ActivityManager.isOnHomeActivity(): Boolean? {
+ try {
+ Trace.beginSection("isOnHomeActivity")
+ return getRunningTasks(/* maxNum= */ 1)?.firstOrNull()?.isHomeActivity()
+ } finally {
+ Trace.endSection()
+ }
+ }
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt
index 24ae42a..fe607e1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt
@@ -19,7 +19,7 @@
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
import com.android.systemui.unfold.config.UnfoldTransitionConfig
-import com.android.systemui.unfold.dagger.UnfoldBackground
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
@@ -56,6 +56,6 @@
abstract fun mainHandler(@Main handler: Handler): Handler
@Binds
- @UnfoldBackground
+ @UnfoldSingleThreadBg
abstract fun backgroundExecutor(@UiBackground executor: Executor): Executor
}
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
index 05372fe..31234cf 100644
--- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
@@ -35,7 +35,7 @@
teamfood: Boolean = false
): UnreleasedFlag {
val flag = UnreleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
- FlagsFactory.checkForDupesAndAdd(flag)
+ checkForDupesAndAdd(flag)
return flag
}
@@ -46,7 +46,7 @@
teamfood: Boolean = false
): ReleasedFlag {
val flag = ReleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
- FlagsFactory.checkForDupesAndAdd(flag)
+ checkForDupesAndAdd(flag)
return flag
}
@@ -65,7 +65,7 @@
resourceId = resourceId,
teamfood = teamfood
)
- FlagsFactory.checkForDupesAndAdd(flag)
+ checkForDupesAndAdd(flag)
return flag
}
@@ -77,18 +77,13 @@
): SysPropBooleanFlag {
val flag =
SysPropBooleanFlag(id = id, name = name, namespace = "systemui", default = default)
- FlagsFactory.checkForDupesAndAdd(flag)
+ checkForDupesAndAdd(flag)
return flag
}
private fun checkForDupesAndAdd(flag: Flag<*>) {
if (flagMap.containsKey(flag.name)) {
- throw IllegalArgumentException("Name {flag.name} is already registered")
- }
- flagMap.forEach {
- if (it.value.id == flag.id) {
- throw IllegalArgumentException("Name {flag.id} is already registered")
- }
+ throw IllegalArgumentException("Name {$flag.name} is already registered")
}
flagMap[flag.name] = flag
}
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 8f38e58..3a940e9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -24,6 +24,7 @@
import android.text.format.DateFormat
import android.util.TypedValue
import android.view.View
+import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
@@ -38,25 +39,25 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.log.dagger.KeyguardClockLog
+import com.android.systemui.log.dagger.KeyguardSmallClockLog
+import com.android.systemui.log.dagger.KeyguardLargeClockLog
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
import com.android.systemui.shared.regionsampling.RegionSampler
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
import com.android.systemui.statusbar.policy.ConfigurationController
-import java.io.PrintWriter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.Executor
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.launch
/**
* Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
@@ -73,19 +74,25 @@
private val context: Context,
@Main private val mainExecutor: Executor,
@Background private val bgExecutor: Executor,
- @KeyguardClockLog private val logBuffer: LogBuffer?,
+ @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?,
+ @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?,
private val featureFlags: FeatureFlags
) {
var clock: ClockController? = null
set(value) {
field = value
if (value != null) {
- if (logBuffer != null) {
- value.setLogBuffer(logBuffer)
- }
+ smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
+ value.smallClock.logBuffer = smallLogBuffer
+ largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" })
+ value.largeClock.logBuffer = largeLogBuffer
value.initialize(resources, dozeAmount, 0f)
- updateRegionSamplers(value)
+
+ if (regionSamplingEnabled) {
+ clock?.smallClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+ clock?.largeClock?.view?.addOnLayoutChangeListener(mLayoutChangedListener)
+ }
updateFontSizes()
}
}
@@ -100,47 +107,87 @@
private var disposableHandle: DisposableHandle? = null
private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
- private fun updateColors() {
+ private val mLayoutChangedListener = object : View.OnLayoutChangeListener {
+ private var currentSmallClockView: View? = null
+ private var currentLargeClockView: View? = null
+ private var currentSmallClockLocation = IntArray(2)
+ private var currentLargeClockLocation = IntArray(2)
- if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
- val wallpaperManager = WallpaperManager.getInstance(context)
- if (!wallpaperManager.lockScreenWallpaperExists()) {
- smallClockIsDark = smallRegionSampler!!.currentRegionDarkness().isDark
- largeClockIsDark = largeRegionSampler!!.currentRegionDarkness().isDark
- }
- } else {
- val isLightTheme = TypedValue()
- context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
- smallClockIsDark = isLightTheme.data == 0
- largeClockIsDark = isLightTheme.data == 0
+ override fun onLayoutChange(
+ view: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ val parent = (view?.parent) as FrameLayout
+
+ // don't pass in negative bounds when clocks are in transition state
+ if (view.locationOnScreen[0] < 0 || view.locationOnScreen[1] < 0) {
+ return
}
+ // SMALL CLOCK
+ if (parent.id == R.id.lockscreen_clock_view) {
+ // view bounds have changed due to clock size changing (i.e. different character widths)
+ // AND/OR the view has been translated when transitioning between small and large clock
+ if (view != currentSmallClockView ||
+ !view.locationOnScreen.contentEquals(currentSmallClockLocation)) {
+ currentSmallClockView = view
+ currentSmallClockLocation = view.locationOnScreen
+ updateRegionSampler(view)
+ }
+ }
+ // LARGE CLOCK
+ else if (parent.id == R.id.lockscreen_clock_view_large) {
+ if (view != currentLargeClockView ||
+ !view.locationOnScreen.contentEquals(currentLargeClockLocation)) {
+ currentLargeClockView = view
+ currentLargeClockLocation = view.locationOnScreen
+ updateRegionSampler(view)
+ }
+ }
+ }
+ }
+
+ private fun updateColors() {
+ val wallpaperManager = WallpaperManager.getInstance(context)
+ if (regionSamplingEnabled && !wallpaperManager.lockScreenWallpaperExists()) {
+ if (regionSampler != null) {
+ if (regionSampler?.sampledView == clock?.smallClock?.view) {
+ smallClockIsDark = regionSampler!!.currentRegionDarkness().isDark
+ clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark)
+ return
+ } else if (regionSampler?.sampledView == clock?.largeClock?.view) {
+ largeClockIsDark = regionSampler!!.currentRegionDarkness().isDark
+ clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark)
+ return
+ }
+ }
+ }
+
+ val isLightTheme = TypedValue()
+ context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
+ smallClockIsDark = isLightTheme.data == 0
+ largeClockIsDark = isLightTheme.data == 0
+
clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark)
clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark)
}
- private fun updateRegionSamplers(currentClock: ClockController?) {
- smallRegionSampler?.stopRegionSampler()
- largeRegionSampler?.stopRegionSampler()
-
- smallRegionSampler = createRegionSampler(
- currentClock?.smallClock?.view,
- mainExecutor,
- bgExecutor,
- regionSamplingEnabled,
- ::updateColors
- )
-
- largeRegionSampler = createRegionSampler(
- currentClock?.largeClock?.view,
- mainExecutor,
- bgExecutor,
- regionSamplingEnabled,
- ::updateColors
- )
-
- smallRegionSampler!!.startRegionSampler()
- largeRegionSampler!!.startRegionSampler()
+ private fun updateRegionSampler(sampledRegion: View) {
+ regionSampler?.stopRegionSampler()
+ regionSampler = createRegionSampler(
+ sampledRegion,
+ mainExecutor,
+ bgExecutor,
+ regionSamplingEnabled,
+ ::updateColors
+ )?.apply { startRegionSampler() }
updateColors()
}
@@ -151,7 +198,7 @@
bgExecutor: Executor?,
regionSamplingEnabled: Boolean,
updateColors: () -> Unit
- ): RegionSampler {
+ ): RegionSampler? {
return RegionSampler(
sampledView,
mainExecutor,
@@ -160,8 +207,7 @@
updateColors)
}
- var smallRegionSampler: RegionSampler? = null
- var largeRegionSampler: RegionSampler? = null
+ var regionSampler: RegionSampler? = null
private var smallClockIsDark = true
private var largeClockIsDark = true
@@ -228,8 +274,6 @@
configurationController.addCallback(configListener)
batteryController.addCallback(batteryCallback)
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
- smallRegionSampler?.startRegionSampler()
- largeRegionSampler?.startRegionSampler()
disposableHandle = parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
@@ -254,8 +298,7 @@
configurationController.removeCallback(configListener)
batteryController.removeCallback(batteryCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
- smallRegionSampler?.stopRegionSampler()
- largeRegionSampler?.stopRegionSampler()
+ regionSampler?.stopRegionSampler()
}
private fun updateFontSizes() {
@@ -265,16 +308,6 @@
resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat())
}
- /**
- * Dump information for debugging
- */
- fun dump(pw: PrintWriter) {
- pw.println(this)
- clock?.dump(pw)
- smallRegionSampler?.dump(pw)
- largeRegionSampler?.dump(pw)
- }
-
@VisibleForTesting
internal fun listenForDozeAmount(scope: CoroutineScope): Job {
return scope.launch {
@@ -325,4 +358,8 @@
}
}
}
+
+ companion object {
+ private val TAG = ClockEventController::class.simpleName!!
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index 5bb9367..e0cf7b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -50,6 +50,7 @@
import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED
import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED
import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED
+import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED
import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN
import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN
import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE
@@ -126,6 +127,7 @@
const val STRONG_AUTH_ALLOWED_CHANGED = "Face auth stopped because strong auth allowed changed"
const val NON_STRONG_BIOMETRIC_ALLOWED_CHANGED =
"Face auth stopped because non strong biometric allowed changed"
+ const val POSTURE_CHANGED = "Face auth started/stopped due to device posture changed."
}
/**
@@ -173,6 +175,7 @@
return PowerManager.wakeReasonToString(extraInfo)
}
},
+ @UiEvent(doc = POSTURE_CHANGED) FACE_AUTH_UPDATED_POSTURE_CHANGED(1265, POSTURE_CHANGED),
@Deprecated(
"Not a face auth trigger.",
ReplaceWith(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index 7da27b1..baaef19 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -103,6 +103,7 @@
@Override
public void reset() {
+ super.reset();
// start fresh
mDismissing = false;
mView.resetPasswordText(false /* animate */, false /* announce */);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 62babad..4acbb0a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -7,7 +7,6 @@
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -20,11 +19,15 @@
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.plugins.ClockController;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import kotlin.Unit;
+
/**
* Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
*/
@@ -87,6 +90,7 @@
private int mClockSwitchYAmount;
@VisibleForTesting boolean mChildrenAreLaidOut = false;
@VisibleForTesting boolean mAnimateOnLayout = true;
+ private LogBuffer mLogBuffer = null;
public KeyguardClockSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -113,6 +117,14 @@
onDensityOrFontScaleChanged();
}
+ public void setLogBuffer(LogBuffer logBuffer) {
+ mLogBuffer = logBuffer;
+ }
+
+ public LogBuffer getLogBuffer() {
+ return mLogBuffer;
+ }
+
void setClock(ClockController clock, int statusBarState) {
mClock = clock;
@@ -121,12 +133,16 @@
mLargeClockFrame.removeAllViews();
if (clock == null) {
- Log.e(TAG, "No clock being shown");
+ if (mLogBuffer != null) {
+ mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown");
+ }
return;
}
// Attach small and big clock views to hierarchy.
- Log.i(TAG, "Attached new clock views to switch");
+ if (mLogBuffer != null) {
+ mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch");
+ }
mSmallClockFrame.addView(clock.getSmallClock().getView());
mLargeClockFrame.addView(clock.getLargeClock().getView());
updateClockTargetRegions();
@@ -152,8 +168,18 @@
}
private void updateClockViews(boolean useLargeClock, boolean animate) {
- Log.i(TAG, "updateClockViews; useLargeClock=" + useLargeClock + "; animate=" + animate
- + "; mChildrenAreLaidOut=" + mChildrenAreLaidOut);
+ if (mLogBuffer != null) {
+ mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> {
+ msg.setBool1(useLargeClock);
+ msg.setBool2(animate);
+ msg.setBool3(mChildrenAreLaidOut);
+ return Unit.INSTANCE;
+ }, (msg) -> "updateClockViews"
+ + "; useLargeClock=" + msg.getBool1()
+ + "; animate=" + msg.getBool2()
+ + "; mChildrenAreLaidOut=" + msg.getBool3());
+ }
+
if (mClockInAnim != null) mClockInAnim.cancel();
if (mClockOutAnim != null) mClockOutAnim.cancel();
if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
@@ -183,6 +209,7 @@
if (!animate) {
out.setAlpha(0f);
+ out.setVisibility(INVISIBLE);
in.setAlpha(1f);
in.setVisibility(VISIBLE);
mStatusArea.setTranslationY(statusAreaYTranslation);
@@ -198,7 +225,10 @@
direction * -mClockSwitchYAmount));
mClockOutAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
- mClockOutAnim = null;
+ if (mClockOutAnim == animation) {
+ out.setVisibility(INVISIBLE);
+ mClockOutAnim = null;
+ }
}
});
@@ -212,7 +242,9 @@
mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2);
mClockInAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
- mClockInAnim = null;
+ if (mClockInAnim == animation) {
+ mClockInAnim = null;
+ }
}
});
@@ -225,7 +257,9 @@
mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
- mStatusAreaAnim = null;
+ if (mStatusAreaAnim == animation) {
+ mStatusAreaAnim = null;
+ }
}
});
mStatusAreaAnim.start();
@@ -269,7 +303,9 @@
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardClockSwitch:");
pw.println(" mSmallClockFrame: " + mSmallClockFrame);
+ pw.println(" mSmallClockFrame.alpha: " + mSmallClockFrame.getAlpha());
pw.println(" mLargeClockFrame: " + mLargeClockFrame);
+ pw.println(" mLargeClockFrame.alpha: " + mLargeClockFrame.getAlpha());
pw.println(" mStatusArea: " + mStatusArea);
pw.println(" mDisplayedClockSize: " + mDisplayedClockSize);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 788f120..8de097c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -38,10 +38,14 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.log.dagger.KeyguardClockLog;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.plugins.ClockController;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.ClockRegistry;
+import com.android.systemui.shared.regionsampling.RegionSampler;
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -62,6 +66,8 @@
*/
public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch>
implements Dumpable {
+ private static final String TAG = "KeyguardClockSwitchController";
+
private final StatusBarStateController mStatusBarStateController;
private final ClockRegistry mClockRegistry;
private final KeyguardSliceViewController mKeyguardSliceViewController;
@@ -70,6 +76,7 @@
private final SecureSettings mSecureSettings;
private final DumpManager mDumpManager;
private final ClockEventController mClockEventController;
+ private final LogBuffer mLogBuffer;
private FrameLayout mSmallClockFrame; // top aligned clock
private FrameLayout mLargeClockFrame; // centered clock
@@ -82,7 +89,10 @@
private final ClockRegistry.ClockChangeListener mClockChangedListener;
private ViewGroup mStatusArea;
- // If set will replace keyguard_slice_view
+
+ // If the SMARTSPACE flag is set, keyguard_slice_view is replaced by the following views.
+ private ViewGroup mDateWeatherView;
+ private View mWeatherView;
private View mSmartspaceView;
private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
@@ -119,7 +129,8 @@
SecureSettings secureSettings,
@Main Executor uiExecutor,
DumpManager dumpManager,
- ClockEventController clockEventController) {
+ ClockEventController clockEventController,
+ @KeyguardClockLog LogBuffer logBuffer) {
super(keyguardClockSwitch);
mStatusBarStateController = statusBarStateController;
mClockRegistry = clockRegistry;
@@ -131,6 +142,8 @@
mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
mDumpManager = dumpManager;
mClockEventController = clockEventController;
+ mLogBuffer = logBuffer;
+ mView.setLogBuffer(mLogBuffer);
mClockChangedListener = () -> {
setClock(mClockRegistry.createCurrentClock());
@@ -183,14 +196,21 @@
if (mSmartspaceController.isEnabled()) {
View ksv = mView.findViewById(R.id.keyguard_slice_view);
- int ksvIndex = mStatusArea.indexOfChild(ksv);
+ int viewIndex = mStatusArea.indexOfChild(ksv);
ksv.setVisibility(View.GONE);
- addSmartspaceView(ksvIndex);
+ // TODO(b/261757708): add content observer for the Settings toggle and add/remove
+ // weather according to the Settings.
+ if (mSmartspaceController.isDateWeatherDecoupled()) {
+ addDateWeatherView(viewIndex);
+ viewIndex += 1;
+ }
+
+ addSmartspaceView(viewIndex);
}
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+ Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
false, /* notifyForDescendants */
mDoubleLineClockObserver,
UserHandle.USER_ALL
@@ -220,6 +240,14 @@
void onLocaleListChanged() {
if (mSmartspaceController.isEnabled()) {
+ if (mSmartspaceController.isDateWeatherDecoupled()) {
+ mDateWeatherView.removeView(mWeatherView);
+ int index = mStatusArea.indexOfChild(mDateWeatherView);
+ if (index >= 0) {
+ mStatusArea.removeView(mDateWeatherView);
+ addDateWeatherView(index);
+ }
+ }
int index = mStatusArea.indexOfChild(mSmartspaceView);
if (index >= 0) {
mStatusArea.removeView(mSmartspaceView);
@@ -228,6 +256,30 @@
}
}
+ private void addDateWeatherView(int index) {
+ mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ MATCH_PARENT, WRAP_CONTENT);
+ mStatusArea.addView(mDateWeatherView, index, lp);
+ int startPadding = getContext().getResources().getDimensionPixelSize(
+ R.dimen.below_clock_padding_start);
+ int endPadding = getContext().getResources().getDimensionPixelSize(
+ R.dimen.below_clock_padding_end);
+ mDateWeatherView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+ addWeatherView();
+ }
+
+ private void addWeatherView() {
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ WRAP_CONTENT, WRAP_CONTENT);
+ mWeatherView = mSmartspaceController.buildAndConnectWeatherView(mView);
+ // Place weather right after the date, before the extras
+ final int index = mDateWeatherView.getChildCount() == 0 ? 0 : 1;
+ mDateWeatherView.addView(mWeatherView, index, lp);
+ mWeatherView.setPaddingRelative(0, 0, 4, 0);
+ }
+
private void addSmartspaceView(int index) {
mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
@@ -337,10 +389,6 @@
int clockHeight = clock.getLargeClock().getView().getHeight();
return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2;
} else {
- // This is only called if we've never shown the large clock as the frame is inflated
- // with 'gone', but then the visibility is never set when it is animated away by
- // KeyguardClockSwitch, instead it is removed from the view hierarchy.
- // TODO(b/261755021): Cleanup Large Frame Visibility
int clockHeight = clock.getSmallClock().getView().getHeight();
return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin;
}
@@ -358,15 +406,11 @@
if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
return clock.getLargeClock().getView().getHeight();
} else {
- // Is not called except in certain edge cases, see comment in getClockBottom
- // TODO(b/261755021): Cleanup Large Frame Visibility
return clock.getSmallClock().getView().getHeight();
}
}
boolean isClockTopAligned() {
- // Returns false except certain edge cases, see comment in getClockBottom
- // TODO(b/261755021): Cleanup Large Frame Visibility
return mLargeClockFrame.getVisibility() != View.VISIBLE;
}
@@ -378,6 +422,10 @@
}
private void setClock(ClockController clock) {
+ if (clock != null && mLogBuffer != null) {
+ mLogBuffer.log(TAG, LogLevel.INFO, "New Clock");
+ }
+
mClockEventController.setClock(clock);
mView.setClock(clock, mStatusBarStateController.getState());
}
@@ -419,6 +467,10 @@
if (clock != null) {
clock.dump(pw);
}
+ final RegionSampler regionSampler = mClockEventController.getRegionSampler();
+ if (regionSampler != null) {
+ regionSampler.dump(pw);
+ }
}
/** Gets the animations for the current clock. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index 02776a2..ec8fa92 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -15,8 +15,6 @@
*/
package com.android.keyguard;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import android.app.Presentation;
import android.content.Context;
import android.graphics.Color;
@@ -37,9 +35,11 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.dagger.KeyguardStatusViewComponent;
import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationBarView;
+import com.android.systemui.settings.DisplayTracker;
import java.util.concurrent.Executor;
@@ -53,6 +53,7 @@
private MediaRouter mMediaRouter = null;
private final DisplayManager mDisplayService;
+ private final DisplayTracker mDisplayTracker;
private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
private final Context mContext;
@@ -62,46 +63,43 @@
private final SparseArray<Presentation> mPresentations = new SparseArray<>();
- private final DisplayManager.DisplayListener mDisplayListener =
- new DisplayManager.DisplayListener() {
+ private final DisplayTracker.Callback mDisplayCallback =
+ new DisplayTracker.Callback() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ Trace.beginSection(
+ "KeyguardDisplayManager#onDisplayAdded(displayId=" + displayId + ")");
+ final Display display = mDisplayService.getDisplay(displayId);
+ if (mShowing) {
+ updateNavigationBarVisibility(displayId, false /* navBarVisible */);
+ showPresentation(display);
+ }
+ Trace.endSection();
+ }
- @Override
- public void onDisplayAdded(int displayId) {
- Trace.beginSection(
- "KeyguardDisplayManager#onDisplayAdded(displayId=" + displayId + ")");
- final Display display = mDisplayService.getDisplay(displayId);
- if (mShowing) {
- updateNavigationBarVisibility(displayId, false /* navBarVisible */);
- showPresentation(display);
- }
- Trace.endSection();
- }
-
- @Override
- public void onDisplayChanged(int displayId) {
-
- }
-
- @Override
- public void onDisplayRemoved(int displayId) {
- Trace.beginSection(
- "KeyguardDisplayManager#onDisplayRemoved(displayId=" + displayId + ")");
- hidePresentation(displayId);
- Trace.endSection();
- }
- };
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ Trace.beginSection(
+ "KeyguardDisplayManager#onDisplayRemoved(displayId=" + displayId + ")");
+ hidePresentation(displayId);
+ Trace.endSection();
+ }
+ };
@Inject
public KeyguardDisplayManager(Context context,
Lazy<NavigationBarController> navigationBarControllerLazy,
KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
+ DisplayTracker displayTracker,
+ @Main Executor mainExecutor,
@UiBackground Executor uiBgExecutor) {
mContext = context;
mNavigationBarControllerLazy = navigationBarControllerLazy;
mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
uiBgExecutor.execute(() -> mMediaRouter = mContext.getSystemService(MediaRouter.class));
mDisplayService = mContext.getSystemService(DisplayManager.class);
- mDisplayService.registerDisplayListener(mDisplayListener, null /* handler */);
+ mDisplayTracker = displayTracker;
+ mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor);
}
private boolean isKeyguardShowable(Display display) {
@@ -109,7 +107,7 @@
if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
return false;
}
- if (display.getDisplayId() == DEFAULT_DISPLAY) {
+ if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
return false;
}
@@ -224,7 +222,7 @@
protected boolean updateDisplays(boolean showing) {
boolean changed = false;
if (showing) {
- final Display[] displays = mDisplayService.getDisplays();
+ final Display[] displays = mDisplayTracker.getAllDisplays();
for (Display display : displays) {
int displayId = display.getDisplayId();
updateNavigationBarVisibility(displayId, false /* navBarVisible */);
@@ -247,7 +245,7 @@
// term solution in R.
private void updateNavigationBarVisibility(int displayId, boolean navBarVisible) {
// Leave this task to {@link StatusBarKeyguardViewManager}
- if (displayId == DEFAULT_DISPLAY) return;
+ if (displayId == mDisplayTracker.getDefaultDisplayId()) return;
NavigationBarView navBarView = mNavigationBarControllerLazy.get()
.getNavigationBarView(displayId);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
index deead19..1a06b5f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
@@ -39,6 +39,7 @@
var keyguardGoingAway: Boolean = false,
var listeningForFaceAssistant: Boolean = false,
var occludingAppRequestingFaceAuth: Boolean = false,
+ val postureAllowsListening: Boolean = false,
var primaryUser: Boolean = false,
var secureCameraLaunched: Boolean = false,
var supportsDetect: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
index 08e9cf6..2a389b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.widget.FrameLayout;
/**
@@ -33,7 +34,7 @@
public class KeyguardHostView extends FrameLayout {
protected ViewMediatorCallback mViewMediatorCallback;
-
+ private boolean mIsInteractable;
public KeyguardHostView(Context context) {
this(context, null);
@@ -54,4 +55,24 @@
public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) {
mViewMediatorCallback = viewMediatorCallback;
}
+
+ /** Set true if the view can be interacted with */
+ public void setInteractable(boolean isInteractable) {
+ mIsInteractable = isInteractable;
+ }
+
+ /**
+ * Make sure to disallow touches while transitioning the bouncer, otherwise
+ * it can remain interactable even when barely visible.
+ */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return !mIsInteractable;
+ }
+
+ /** True to consume any events that are sent to it */
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return true;
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
index d4ca8e3..6139403 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
@@ -29,6 +29,9 @@
import android.view.View.OnKeyListener;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
+import android.window.OnBackAnimationCallback;
+
+import androidx.annotation.NonNull;
import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
@@ -394,6 +397,14 @@
}
/**
+ * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture.
+ */
+ @NonNull
+ public OnBackAnimationCallback getBackCallback() {
+ return mKeyguardSecurityContainerController.getBackCallback();
+ }
+
+ /**
* Allows the media keys to work when the keyguard is showing.
* The media keys should be of no interest to the actual keyguard view(s),
* so intercepting them here should not be of any harm.
@@ -516,4 +527,9 @@
mKeyguardSecurityContainerController.updateKeyguardPosition(x);
}
}
+
+ /** Set true if the view can be interacted with */
+ public void setInteractable(boolean isInteractable) {
+ mView.setInteractable(isInteractable);
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index d1c9a30..b143c5b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -121,6 +121,7 @@
@Override
public void reset() {
+ mMessageAreaController.setMessage("", false);
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 5d7a6f1..9f07a20 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -32,6 +32,7 @@
import static androidx.constraintlayout.widget.ConstraintSet.TOP;
import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
+import static com.android.systemui.animation.InterpolatorsAndroidX.DECELERATE_QUINT;
import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
import static java.lang.Integer.max;
@@ -73,6 +74,8 @@
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
@@ -89,6 +92,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.settingslib.Utils;
+import com.android.settingslib.drawable.CircleFramedDrawable;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
@@ -135,7 +139,9 @@
private static final float MIN_DRAG_SIZE = 10;
// How much to scale the default slop by, to avoid accidental drags.
private static final float SLOP_SCALE = 4f;
-
+ @VisibleForTesting
+ // How much the view scales down to during back gestures.
+ static final float MIN_BACK_SCALE = 0.9f;
@VisibleForTesting
KeyguardSecurityViewFlipper mSecurityViewFlipper;
private GlobalSettings mGlobalSettings;
@@ -230,16 +236,35 @@
}
updateChildren(0 /* translationY */, 1f /* alpha */);
}
-
- private void updateChildren(int translationY, float alpha) {
- for (int i = 0; i < KeyguardSecurityContainer.this.getChildCount(); ++i) {
- View child = KeyguardSecurityContainer.this.getChildAt(i);
- child.setTranslationY(translationY);
- child.setAlpha(alpha);
- }
- }
};
+ private final OnBackAnimationCallback mBackCallback = new OnBackAnimationCallback() {
+ @Override
+ public void onBackCancelled() {
+ // TODO(b/259608500): Remove once back API auto animates progress to 0 on cancel.
+ resetScale();
+ }
+
+ @Override
+ public void onBackInvoked() { }
+
+ @Override
+ public void onBackProgressed(BackEvent event) {
+ float progress = event.getProgress();
+ // TODO(b/263819310): Update the interpolator to match spec.
+ float scale = MIN_BACK_SCALE
+ + (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress));
+ setScale(scale);
+ }
+ };
+ /**
+ * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture.
+ */
+ @NonNull
+ OnBackAnimationCallback getBackCallback() {
+ return mBackCallback;
+ }
+
// Used to notify the container when something interesting happens.
public interface SecurityCallback {
/**
@@ -562,6 +587,7 @@
* This will run when the bouncer shows in all cases except when the user drags the bouncer up.
*/
public void startAppearAnimation(SecurityMode securityMode) {
+ updateChildren(0 /* translationY */, 1f /* alpha */);
mViewMode.startAppearAnimation(securityMode);
}
@@ -736,6 +762,23 @@
mViewMode.onDensityOrFontScaleChanged();
}
+ void resetScale() {
+ setScale(1);
+ }
+
+ private void setScale(float scale) {
+ setScaleX(scale);
+ setScaleY(scale);
+ }
+
+ private void updateChildren(int translationY, float alpha) {
+ for (int i = 0; i < getChildCount(); ++i) {
+ View child = getChildAt(i);
+ child.setTranslationY(translationY);
+ child.setAlpha(alpha);
+ }
+ }
+
/**
* Enscapsulates the differences between bouncer modes for the container.
*/
@@ -957,8 +1000,10 @@
private Drawable findUserIcon(int userId) {
Bitmap userIcon = UserManager.get(mView.getContext()).getUserIcon(userId);
if (userIcon != null) {
- return new BitmapDrawable(userIcon);
+ return CircleFramedDrawable.getInstance(mView.getContext(),
+ userIcon);
}
+
return UserIcons.getDefaultUserIcon(mResources, userId, false);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index a72a484..57bfe54 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -40,7 +40,9 @@
import android.util.Slog;
import android.view.MotionEvent;
import android.view.View;
+import android.window.OnBackAnimationCallback;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
@@ -479,6 +481,9 @@
/** Called when the bouncer changes visibility. */
public void onBouncerVisibilityChanged(@View.Visibility int visibility) {
setBouncerVisible(visibility == View.VISIBLE);
+ if (visibility == View.INVISIBLE) {
+ mView.resetScale();
+ }
}
private void setBouncerVisible(boolean visible) {
@@ -588,6 +593,14 @@
}
/**
+ * @return the {@link OnBackAnimationCallback} to animate this view during a back gesture.
+ */
+ @NonNull
+ OnBackAnimationCallback getBackCallback() {
+ return mView.getBackCallback();
+ }
+
+ /**
* Switches to the given security view unless it's already being shown, in which case
* this is a no-op.
*
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
index 0b2b121..e3de8c7 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
@@ -17,7 +17,6 @@
package com.android.keyguard;
import static android.app.slice.Slice.HINT_LIST_ITEM;
-import static android.view.Display.DEFAULT_DISPLAY;
import android.app.PendingIntent;
import android.net.Uri;
@@ -43,6 +42,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.ViewController;
@@ -64,6 +64,7 @@
private final ConfigurationController mConfigurationController;
private final TunerService mTunerService;
private final DumpManager mDumpManager;
+ private final DisplayTracker mDisplayTracker;
private int mDisplayId;
private LiveData<Slice> mLiveData;
private Uri mKeyguardSliceUri;
@@ -108,12 +109,14 @@
ActivityStarter activityStarter,
ConfigurationController configurationController,
TunerService tunerService,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ DisplayTracker displayTracker) {
super(keyguardSliceView);
mActivityStarter = activityStarter;
mConfigurationController = configurationController;
mTunerService = tunerService;
mDumpManager = dumpManager;
+ mDisplayTracker = displayTracker;
}
@Override
@@ -124,7 +127,7 @@
}
mTunerService.addTunable(mTunable, Settings.Secure.KEYGUARD_SLICE_URI);
// Make sure we always have the most current slice
- if (mDisplayId == DEFAULT_DISPLAY && mLiveData != null) {
+ if (mDisplayId == mDisplayTracker.getDefaultDisplayId() && mLiveData != null) {
mLiveData.observeForever(mObserver);
}
mConfigurationController.addCallback(mConfigurationListener);
@@ -137,7 +140,7 @@
@Override
protected void onViewDetached() {
// TODO(b/117344873) Remove below work around after this issue be fixed.
- if (mDisplayId == DEFAULT_DISPLAY) {
+ if (mDisplayId == mDisplayTracker.getDefaultDisplayId()) {
mLiveData.removeObserver(mObserver);
}
mTunerService.removeTunable(mTunable);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index aec3063..b53b868 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,7 @@
import android.util.Slog;
import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ClockAnimations;
@@ -62,14 +63,16 @@
ConfigurationController configurationController,
DozeParameters dozeParameters,
FeatureFlags featureFlags,
- ScreenOffAnimationController screenOffAnimationController) {
+ ScreenOffAnimationController screenOffAnimationController,
+ KeyguardLogger logger) {
super(keyguardStatusView);
mKeyguardSliceViewController = keyguardSliceViewController;
mKeyguardClockSwitchController = keyguardClockSwitchController;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mConfigurationController = configurationController;
mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
- dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
+ dozeParameters, screenOffAnimationController, /* animateYPos= */ true,
+ logger.getBuffer());
mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 93027c1..54886c3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -26,7 +26,6 @@
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_PERMANENT;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED;
import static android.hardware.biometrics.BiometricConstants.LockoutMode;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static android.hardware.biometrics.BiometricSourceType.FACE;
import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN;
@@ -63,11 +62,13 @@
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_KEYGUARD_INIT;
+import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING;
import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
import android.annotation.AnyThread;
import android.annotation.MainThread;
@@ -84,7 +85,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
@@ -108,7 +108,6 @@
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
@@ -141,6 +140,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -154,6 +154,7 @@
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.telephony.TelephonyListenerManager;
import com.android.systemui.util.Assert;
import com.android.systemui.util.settings.SecureSettings;
@@ -170,6 +171,7 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.Executor;
@@ -269,21 +271,6 @@
private static final ComponentName FALLBACK_HOME_COMPONENT = new ComponentName(
"com.android.settings", "com.android.settings.FallbackHome");
- /**
- * If true, the system is in the half-boot-to-decryption-screen state.
- * Prudently disable lockscreen.
- */
- public static final boolean CORE_APPS_ONLY;
-
- static {
- try {
- CORE_APPS_ONLY = IPackageManager.Stub.asInterface(
- ServiceManager.getService("package")).isOnlyCoreApps();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
private final Context mContext;
private final UserTracker mUserTracker;
private final KeyguardUpdateMonitorLogger mLogger;
@@ -345,18 +332,17 @@
private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
mCallbacks = Lists.newArrayList();
private ContentObserver mDeviceProvisionedObserver;
- private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver;
private final ContentObserver mTimeFormatChangeObserver;
private boolean mSwitchingUser;
private boolean mDeviceInteractive;
- private boolean mSfpsRequireScreenOnToAuthPrefEnabled;
private final SubscriptionManager mSubscriptionManager;
private final TelephonyListenerManager mTelephonyListenerManager;
private final TrustManager mTrustManager;
private final UserManager mUserManager;
private final DevicePolicyManager mDevicePolicyManager;
+ private final DevicePostureController mPostureController;
private final BroadcastDispatcher mBroadcastDispatcher;
private final SecureSettings mSecureSettings;
private final InteractionJankMonitor mInteractionJankMonitor;
@@ -365,7 +351,6 @@
private final Executor mBackgroundExecutor;
private final SensorPrivacyManager mSensorPrivacyManager;
private final ActiveUnlockConfig mActiveUnlockConfig;
- private final PowerManager mPowerManager;
private final IDreamManager mDreamManager;
private final TelephonyManager mTelephonyManager;
@Nullable
@@ -373,7 +358,9 @@
@Nullable
private final FaceManager mFaceManager;
private final LockPatternUtils mLockPatternUtils;
- private final boolean mWakeOnFingerprintAcquiredStart;
+ @VisibleForTesting
+ @DevicePostureController.DevicePostureInt
+ protected int mConfigFaceAuthSupportedPosture;
private KeyguardBypassController mKeyguardBypassController;
private List<SubscriptionInfo> mSubscriptionInfo;
@@ -384,6 +371,8 @@
private boolean mLogoutEnabled;
private boolean mIsFaceEnrolled;
private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ private int mPostureState = DEVICE_POSTURE_UNKNOWN;
+ private FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider;
/**
* Short delay before restarting fingerprint authentication after a successful try. This should
@@ -711,8 +700,18 @@
*/
public void setKeyguardGoingAway(boolean goingAway) {
mKeyguardGoingAway = goingAway;
- // This is set specifically to stop face authentication from running.
- updateBiometricListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY);
+ if (mKeyguardGoingAway) {
+ updateFaceListeningState(BIOMETRIC_ACTION_STOP,
+ FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY);
+ }
+ updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+ }
+
+ /**
+ * Whether keyguard is going away due to screen off or device entry.
+ */
+ public boolean isKeyguardGoingAway() {
+ return mKeyguardGoingAway;
}
/**
@@ -882,11 +881,6 @@
private void handleFingerprintAcquired(
@BiometricFingerprintConstants.FingerprintAcquired int acquireInfo) {
Assert.isMainThread();
- if (mWakeOnFingerprintAcquiredStart && acquireInfo == FINGERPRINT_ACQUIRED_START) {
- mPowerManager.wakeUp(
- SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
- "com.android.systemui.keyguard:FINGERPRINT_ACQUIRED_START");
- }
for (int i = 0; i < mCallbacks.size(); i++) {
KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
if (cb != null) {
@@ -1535,6 +1529,7 @@
@VisibleForTesting
void setAssistantVisible(boolean assistantVisible) {
mAssistantVisible = assistantVisible;
+ mLogger.logAssistantVisible(mAssistantVisible);
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED);
if (mAssistantVisible) {
@@ -1784,6 +1779,17 @@
};
@VisibleForTesting
+ final DevicePostureController.Callback mPostureCallback =
+ new DevicePostureController.Callback() {
+ @Override
+ public void onPostureChanged(int posture) {
+ mPostureState = posture;
+ updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+ FACE_AUTH_UPDATED_POSTURE_CHANGED);
+ }
+ };
+
+ @VisibleForTesting
CancellationSignal mFingerprintCancelSignal;
@VisibleForTesting
CancellationSignal mFaceCancelSignal;
@@ -1931,6 +1937,11 @@
}
}
mGoingToSleep = true;
+ // Resetting assistant visibility state as the device is going to sleep now.
+ // TaskStackChangeListener gets triggered a little late when we transition to AoD,
+ // which results in face auth running once on AoD.
+ mAssistantVisible = false;
+ mLogger.d("Started going to sleep, mGoingToSleep=true, mAssistantVisible=false");
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_GOING_TO_SLEEP);
}
@@ -1943,9 +1954,9 @@
cb.onFinishedGoingToSleep(arg1);
}
}
- // This is set specifically to stop face authentication from running.
- updateBiometricListeningState(BIOMETRIC_ACTION_STOP,
+ updateFaceListeningState(BIOMETRIC_ACTION_STOP,
FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP);
+ updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
}
private void handleScreenTurnedOff() {
@@ -2036,7 +2047,6 @@
UiEventLogger uiEventLogger,
// This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :(
Provider<SessionTracker> sessionTrackerProvider,
- PowerManager powerManager,
TrustManager trustManager,
SubscriptionManager subscriptionManager,
UserManager userManager,
@@ -2048,7 +2058,9 @@
@Nullable FaceManager faceManager,
@Nullable FingerprintManager fingerprintManager,
@Nullable BiometricManager biometricManager,
- FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) {
+ FaceWakeUpTriggersConfig faceWakeUpTriggersConfig,
+ DevicePostureController devicePostureController,
+ Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) {
mContext = context;
mSubscriptionManager = subscriptionManager;
mUserTracker = userTracker;
@@ -2071,23 +2083,23 @@
mLogger = logger;
mUiEventLogger = uiEventLogger;
mSessionTrackerProvider = sessionTrackerProvider;
- mPowerManager = powerManager;
mTrustManager = trustManager;
mUserManager = userManager;
mDreamManager = dreamManager;
mTelephonyManager = telephonyManager;
mDevicePolicyManager = devicePolicyManager;
+ mPostureController = devicePostureController;
mPackageManager = packageManager;
mFpm = fingerprintManager;
mFaceManager = faceManager;
mActiveUnlockConfig.setKeyguardUpdateMonitor(this);
- mWakeOnFingerprintAcquiredStart = context.getResources()
- .getBoolean(com.android.internal.R.bool.kg_wake_on_acquire_start);
mFaceAcquiredInfoIgnoreList = Arrays.stream(
mContext.getResources().getIntArray(
R.array.config_face_acquire_device_entry_ignorelist))
.boxed()
.collect(Collectors.toSet());
+ mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger(
+ R.integer.config_face_auth_supported_posture);
mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig;
mHandler = new Handler(mainLooper) {
@@ -2278,6 +2290,9 @@
FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED));
}
});
+ if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) {
+ mPostureController.addCallback(mPostureCallback);
+ }
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT);
TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
@@ -2311,30 +2326,7 @@
Settings.System.getUriFor(Settings.System.TIME_12_24),
false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
- updateSfpsRequireScreenOnToAuthPref();
- mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- updateSfpsRequireScreenOnToAuthPref();
- }
- };
-
- mContext.getContentResolver().registerContentObserver(
- mSecureSettings.getUriFor(
- Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED),
- false,
- mSfpsRequireScreenOnToAuthPrefObserver,
- getCurrentUser());
- }
-
- protected void updateSfpsRequireScreenOnToAuthPref() {
- final int defaultSfpsRequireScreenOnToAuthValue =
- mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0;
- mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser(
- Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
- defaultSfpsRequireScreenOnToAuthValue,
- getCurrentUser()) != 0;
+ mFingerprintInteractiveToAuthProvider = interactiveToAuthProvider.orElse(null);
}
private void initializeSimState() {
@@ -2680,7 +2672,10 @@
private boolean shouldListenForFaceAssistant() {
BiometricAuthenticated face = mUserFaceAuthenticated.get(getCurrentUser());
- return mAssistantVisible && mKeyguardOccluded
+ return mAssistantVisible
+ // There can be intermediate states where mKeyguardShowing is false but
+ // mKeyguardOccluded is true, we don't want to run face auth in such a scenario.
+ && (mKeyguardShowing && mKeyguardOccluded)
&& !(face != null && face.mAuthenticated)
&& !mUserHasTrust.get(getCurrentUser(), false);
}
@@ -2729,8 +2724,11 @@
boolean shouldListenSideFpsState = true;
if (isSideFps) {
+ final boolean interactiveToAuthEnabled =
+ mFingerprintInteractiveToAuthProvider != null &&
+ mFingerprintInteractiveToAuthProvider.isEnabled(getCurrentUser());
shouldListenSideFpsState =
- mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true;
+ interactiveToAuthEnabled ? isDeviceInteractive() && !mGoingToSleep : true;
}
boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
@@ -2742,7 +2740,7 @@
user,
shouldListen,
biometricEnabledForUser,
- mPrimaryBouncerIsOrWillBeShowing,
+ mPrimaryBouncerIsOrWillBeShowing,
userCanSkipBouncer,
mCredentialAttempted,
mDeviceInteractive,
@@ -2802,6 +2800,9 @@
final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user);
final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant();
final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown();
+ final boolean isPostureAllowedForFaceAuth =
+ mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true
+ : (mPostureState == mConfigFaceAuthSupportedPosture);
// Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
// instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
@@ -2818,7 +2819,8 @@
&& faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser
&& (!mSecureCameraLaunched || mOccludingAppRequestingFace)
&& faceAndFpNotAuthenticated
- && !mGoingToSleep;
+ && !mGoingToSleep
+ && isPostureAllowedForFaceAuth;
// Aggregate relevant fields for debug logging.
logListenerModelData(
@@ -2838,6 +2840,7 @@
mKeyguardGoingAway,
shouldListenForFaceAssistant,
mOccludingAppRequestingFace,
+ isPostureAllowedForFaceAuth,
mIsPrimaryUser,
mSecureCameraLaunched,
supportsDetect,
@@ -2923,7 +2926,7 @@
getKeyguardSessionId(),
faceAuthUiEvent.getExtraInfo()
);
-
+ mLogger.logFaceUnlockPossible(unlockPossible);
if (unlockPossible) {
mFaceCancelSignal = new CancellationSignal();
@@ -3332,7 +3335,8 @@
/**
* Handle {@link #MSG_KEYGUARD_RESET}
*/
- private void handleKeyguardReset() {
+ @VisibleForTesting
+ protected void handleKeyguardReset() {
mLogger.d("handleKeyguardReset");
updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
FACE_AUTH_UPDATED_KEYGUARD_RESET);
@@ -3673,6 +3677,7 @@
if (info == null) {
return;
}
+ mLogger.logTaskStackChangedForAssistant(info.visible);
mHandler.sendMessage(mHandler.obtainMessage(MSG_ASSISTANT_STACK_CHANGED,
info.visible));
} catch (RemoteException e) {
@@ -3845,11 +3850,6 @@
mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);
}
- if (mSfpsRequireScreenOnToAuthPrefObserver != null) {
- mContext.getContentResolver().unregisterContentObserver(
- mSfpsRequireScreenOnToAuthPrefObserver);
- }
-
try {
ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
} catch (RemoteException e) {
@@ -3876,7 +3876,6 @@
pw.println(" getUserHasTrust()=" + getUserHasTrust(getCurrentUser()));
pw.println(" getUserUnlockedWithBiometric()="
+ getUserUnlockedWithBiometric(getCurrentUser()));
- pw.println(" mWakeOnFingerprintAcquiredStart=" + mWakeOnFingerprintAcquiredStart);
pw.println(" SIM States:");
for (SimData data : mSimDatas.values()) {
pw.println(" " + data.toString());
@@ -3926,8 +3925,14 @@
} else if (isSfpsSupported()) {
pw.println(" sfpsEnrolled=" + isSfpsEnrolled());
pw.println(" shouldListenForSfps=" + shouldListenForFingerprint(false));
- pw.println(" mSfpsRequireScreenOnToAuthPrefEnabled="
- + mSfpsRequireScreenOnToAuthPrefEnabled);
+ if (isSfpsEnrolled()) {
+ final boolean interactiveToAuthEnabled =
+ mFingerprintInteractiveToAuthProvider != null &&
+ mFingerprintInteractiveToAuthProvider
+ .isEnabled(getCurrentUser());
+ pw.println(" interactiveToAuthEnabled="
+ + interactiveToAuthEnabled);
+ }
}
new DumpsysTableLogger(
"KeyguardFingerprintListen",
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index bde0692..7e48193 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -22,6 +22,8 @@
import android.view.ViewPropertyAnimator;
import com.android.systemui.animation.Interpolators;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -31,11 +33,14 @@
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.google.errorprone.annotations.CompileTimeConstant;
+
/**
* Helper class for updating visibility of keyguard views based on keyguard and status bar state.
* This logic is shared by both the keyguard status view and the keyguard user switcher.
*/
public class KeyguardVisibilityHelper {
+ private static final String TAG = "KeyguardVisibilityHelper";
private View mView;
private final KeyguardStateController mKeyguardStateController;
@@ -46,17 +51,26 @@
private boolean mLastOccludedState = false;
private boolean mIsUnoccludeTransitionFlagEnabled = false;
private final AnimationProperties mAnimationProperties = new AnimationProperties();
+ private final LogBuffer mLogBuffer;
public KeyguardVisibilityHelper(View view,
KeyguardStateController keyguardStateController,
DozeParameters dozeParameters,
ScreenOffAnimationController screenOffAnimationController,
- boolean animateYPos) {
+ boolean animateYPos,
+ LogBuffer logBuffer) {
mView = view;
mKeyguardStateController = keyguardStateController;
mDozeParameters = dozeParameters;
mScreenOffAnimationController = screenOffAnimationController;
mAnimateYPos = animateYPos;
+ mLogBuffer = logBuffer;
+ }
+
+ private void log(@CompileTimeConstant String message) {
+ if (mLogBuffer != null) {
+ mLogBuffer.log(TAG, LogLevel.DEBUG, message);
+ }
}
public boolean isVisibilityAnimating() {
@@ -94,6 +108,9 @@
.setStartDelay(mKeyguardStateController.getKeyguardFadingAwayDelay())
.setDuration(mKeyguardStateController.getShortenedFadingAwayDuration())
.start();
+ log("goingToFullShade && keyguardFadingAway");
+ } else {
+ log("goingToFullShade && !keyguardFadingAway");
}
} else if (oldStatusBarState == StatusBarState.SHADE_LOCKED && statusBarState == KEYGUARD) {
mView.setVisibility(View.VISIBLE);
@@ -105,6 +122,7 @@
.setDuration(320)
.setInterpolator(Interpolators.ALPHA_IN)
.withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
+ log("keyguardFadingAway transition w/ Y Aniamtion");
} else if (statusBarState == KEYGUARD) {
if (keyguardFadingAway) {
mKeyguardViewVisibilityAnimating = true;
@@ -125,9 +143,13 @@
true /* animate */);
animator.setDuration(duration)
.setStartDelay(delay);
+ log("keyguardFadingAway transition w/ Y Aniamtion");
+ } else {
+ log("keyguardFadingAway transition w/o Y Animation");
}
animator.start();
} else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) {
+ log("ScreenOff transition");
mKeyguardViewVisibilityAnimating = true;
// Ask the screen off animation controller to animate the keyguard visibility for us
@@ -136,6 +158,7 @@
mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
} else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
// An activity was displayed over the lock screen, and has now gone away
+ log("Unoccluded transition");
mView.setVisibility(View.VISIBLE);
mView.setAlpha(0f);
@@ -146,12 +169,14 @@
.withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable)
.start();
} else {
+ log("Direct set Visibility to VISIBLE");
mView.setVisibility(View.VISIBLE);
if (!mIsUnoccludeTransitionFlagEnabled) {
mView.setAlpha(1f);
}
}
} else {
+ log("Direct set Visibility to GONE");
mView.setVisibility(View.GONE);
mView.setAlpha(1f);
}
@@ -162,14 +187,18 @@
private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = () -> {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.INVISIBLE);
+ log("Callback Set Visibility to INVISIBLE");
};
private final Runnable mAnimateKeyguardStatusViewGoneEndRunnable = () -> {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.GONE);
+ log("CallbackSet Visibility to GONE");
};
private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> {
mKeyguardViewVisibilityAnimating = false;
+ mView.setVisibility(View.VISIBLE);
+ log("Callback Set Visibility to VISIBLE");
};
}
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 1322f16..8071a5d 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -429,6 +429,7 @@
pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
+ pw.println(" mDefaultPaddingPx: " + mDefaultPaddingPx);
if (mView != null) {
mView.dump(pw, args);
diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
index b159714..35cae09 100644
--- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
+++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java
@@ -47,7 +47,6 @@
import com.android.systemui.R;
import java.util.ArrayList;
-import java.util.Stack;
/**
* A View similar to a textView which contains password text and can animate when the text is
@@ -92,7 +91,6 @@
private final int mGravity;
private ArrayList<CharState> mTextChars = new ArrayList<>();
private String mText = "";
- private Stack<CharState> mCharPool = new Stack<>();
private int mDotSize;
private PowerManager mPM;
private int mCharPadding;
@@ -310,13 +308,7 @@
}
private CharState obtainCharState(char c) {
- CharState charState;
- if(mCharPool.isEmpty()) {
- charState = new CharState();
- } else {
- charState = mCharPool.pop();
- charState.reset();
- }
+ CharState charState = new CharState();
charState.whichChar = c;
return charState;
}
@@ -343,8 +335,6 @@
maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION;
charState.startRemoveAnimation(startDelay, maxDelay);
charState.removeDotSwapCallbacks();
- } else {
- mCharPool.push(charState);
}
}
if (!animated) {
@@ -421,8 +411,6 @@
public void onAnimationEnd(Animator animation) {
if (!mCancelled) {
mTextChars.remove(CharState.this);
- mCharPool.push(CharState.this);
- reset();
cancelAnimator(textTranslateAnimator);
textTranslateAnimator = null;
}
@@ -518,21 +506,6 @@
}
};
- void reset() {
- whichChar = 0;
- currentTextSizeFactor = 0.0f;
- currentDotSizeFactor = 0.0f;
- currentWidthFactor = 0.0f;
- cancelAnimator(textAnimator);
- textAnimator = null;
- cancelAnimator(dotAnimator);
- dotAnimator = null;
- cancelAnimator(widthAnimator);
- widthAnimator = null;
- currentTextTranslationY = 1.0f;
- removeDotSwapCallbacks();
- }
-
void startRemoveAnimation(long startDelay, long widthDelay) {
boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null)
|| (dotAnimator != null && dotAnimationIsGrowing);
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
index 0cbf8bc..5ad21df 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerComponent.java
@@ -20,13 +20,13 @@
import com.android.keyguard.KeyguardHostViewController;
import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import dagger.BindsInstance;
import dagger.Subcomponent;
/**
- * Dagger Subcomponent for the {@link KeyguardBouncer}.
+ * Dagger Subcomponent for the {@link PrimaryBouncerInteractor}.
*/
@Subcomponent(modules = {KeyguardBouncerModule.class})
@KeyguardBouncerScope
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
index ef067b8..cb7a0a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
@@ -29,7 +29,7 @@
import com.android.systemui.R;
import com.android.systemui.biometrics.SideFpsController;
import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import java.util.Optional;
@@ -39,7 +39,7 @@
import dagger.Provides;
/**
- * Module to create and access view related to the {@link KeyguardBouncer}.
+ * Module to create and access view related to the {@link PrimaryBouncerInteractor}.
*/
@Module
public interface KeyguardBouncerModule {
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index b84fb08..2c7eceb 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -17,36 +17,46 @@
package com.android.keyguard.logging
import com.android.systemui.log.dagger.KeyguardLog
-import com.android.systemui.plugins.log.ConstantStringsLogger
-import com.android.systemui.plugins.log.ConstantStringsLoggerImpl
import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel.DEBUG
-import com.android.systemui.plugins.log.LogLevel.ERROR
-import com.android.systemui.plugins.log.LogLevel.INFO
-import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel
import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
-private const val TAG = "KeyguardLog"
+private const val BIO_TAG = "KeyguardLog"
/**
* Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding
* temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be
* an overkill.
*/
-class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) :
- ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) {
+class KeyguardLogger
+@Inject
+constructor(
+ @KeyguardLog val buffer: LogBuffer,
+) {
+ @JvmOverloads
+ fun log(
+ tag: String,
+ level: LogLevel,
+ @CompileTimeConstant msg: String,
+ ex: Throwable? = null,
+ ) = buffer.log(tag, level, msg, ex)
- fun logException(ex: Exception, @CompileTimeConstant logMsg: String) {
- buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex)
- }
-
- fun v(msg: String, arg: Any) {
- buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" })
- }
-
- fun i(msg: String, arg: Any) {
- buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
+ fun log(
+ tag: String,
+ level: LogLevel,
+ @CompileTimeConstant msg: String,
+ arg: Any,
+ ) {
+ buffer.log(
+ tag,
+ level,
+ {
+ str1 = msg
+ str2 = arg.toString()
+ },
+ { "$str1: $str2" }
+ )
}
@JvmOverloads
@@ -56,8 +66,8 @@
msg: String? = null
) {
buffer.log(
- TAG,
- DEBUG,
+ BIO_TAG,
+ LogLevel.DEBUG,
{
str1 = context
str2 = "$msgId"
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 21d3b24..201a1d9 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -132,6 +132,12 @@
logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" })
}
+ fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) {
+ logBuffer.log(TAG, DEBUG,
+ { bool1 = isFaceUnlockPossible },
+ {"isUnlockWithFacePossible: $bool1"})
+ }
+
fun logFingerprintAuthForWrongUser(authUserId: Int) {
logBuffer.log(TAG, DEBUG,
{ int1 = authUserId },
@@ -425,4 +431,20 @@
str1 = PowerManager.wakeReasonToString(pmWakeReason)
}, { "Skip updating face listening state on wakeup from $str1"})
}
+
+ fun logTaskStackChangedForAssistant(assistantVisible: Boolean) {
+ logBuffer.log(TAG, VERBOSE, {
+ bool1 = assistantVisible
+ }, {
+ "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1"
+ })
+ }
+
+ fun logAssistantVisible(assistantVisible: Boolean) {
+ logBuffer.log(TAG, VERBOSE, {
+ bool1 = assistantVisible
+ }, {
+ "Updating mAssistantVisible to new value: $bool1"
+ })
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
new file mode 100644
index 0000000..249b3fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.keyguard.logging
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.TrustModel
+import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+/** Logging helper for trust repository. */
+@SysUISingleton
+class TrustRepositoryLogger
+@Inject
+constructor(
+ @KeyguardUpdateMonitorLog private val logBuffer: LogBuffer,
+) {
+ fun onTrustChanged(
+ enabled: Boolean,
+ newlyUnlocked: Boolean,
+ userId: Int,
+ flags: Int,
+ trustGrantedMessages: List<String>?
+ ) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ bool1 = enabled
+ bool2 = newlyUnlocked
+ int1 = userId
+ int2 = flags
+ str1 = trustGrantedMessages?.joinToString()
+ },
+ {
+ "onTrustChanged enabled: $bool1, newlyUnlocked: $bool2, " +
+ "userId: $int1, flags: $int2, grantMessages: $str1"
+ }
+ )
+ }
+
+ fun trustListenerRegistered() {
+ logBuffer.log(TAG, LogLevel.VERBOSE, "TrustRepository#registerTrustListener")
+ }
+
+ fun trustListenerUnregistered() {
+ logBuffer.log(TAG, LogLevel.VERBOSE, "TrustRepository#unregisterTrustListener")
+ }
+
+ fun trustModelEmitted(value: TrustModel) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ int1 = value.userId
+ bool1 = value.isTrusted
+ },
+ { "trustModel emitted: userId: $int1 isTrusted: $bool1" }
+ )
+ }
+
+ fun isCurrentUserTrusted(isCurrentUserTrusted: Boolean) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { bool1 = isCurrentUserTrusted },
+ { "isCurrentUserTrusted emitted: $bool1" }
+ )
+ }
+
+ companion object {
+ const val TAG = "TrustRepositoryLog"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
index 9ac45b3..227f0ace 100644
--- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -34,7 +34,7 @@
override fun start() {
coroutineScope.launch {
val listener = FlagListenable.Listener { event ->
- if (event.flagId == Flags.CHOOSER_UNBUNDLED.id) {
+ if (event.flagName == Flags.CHOOSER_UNBUNDLED.name) {
launch { updateUnbundledChooserEnabled() }
event.requestNoRestart()
}
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index 3e0fa45..54939fd 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -35,6 +35,7 @@
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.Utils
import com.android.systemui.animation.Interpolators
+import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
@@ -47,7 +48,8 @@
pos: Int,
val statusBarStateController: StatusBarStateController,
val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- val mainExecutor: Executor
+ val mainExecutor: Executor,
+ val logger: ScreenDecorationsLogger,
) : ScreenDecorations.DisplayCutoutView(context, pos) {
private var showScanningAnim = false
private val rimPaint = Paint()
@@ -55,6 +57,7 @@
private var rimAnimator: AnimatorSet? = null
private val rimRect = RectF()
private var cameraProtectionColor = Color.BLACK
+
var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
R.attr.wallpaperTextColorAccent)
private var cameraProtectionAnimator: ValueAnimator? = null
@@ -175,15 +178,22 @@
}
if (showScanningAnim) {
// Make sure that our measured height encompasses the extra space for the animation
- mTotalBounds.union(mBoundingRect)
+ mTotalBounds.set(mBoundingRect)
mTotalBounds.union(
rimRect.left.toInt(),
rimRect.top.toInt(),
rimRect.right.toInt(),
rimRect.bottom.toInt())
- setMeasuredDimension(
- resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
- resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+ val measuredWidth = resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0)
+ val measuredHeight = resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)
+ logger.boundingRect(rimRect, "onMeasure: Face scanning animation")
+ logger.boundingRect(mBoundingRect, "onMeasure: Display cutout view bounding rect")
+ logger.boundingRect(mTotalBounds, "onMeasure: TotalBounds")
+ logger.onMeasureDimensions(widthMeasureSpec,
+ heightMeasureSpec,
+ measuredWidth,
+ measuredHeight)
+ setMeasuredDimension(measuredWidth, measuredHeight)
} else {
setMeasuredDimension(
resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index e6f559b..c1c7f2d 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -36,10 +36,10 @@
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.hardware.display.DisplayManager;
import android.hardware.graphics.common.AlphaInterpretation;
import android.hardware.graphics.common.DisplayDecorationSupport;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.SystemProperties;
import android.os.Trace;
import android.provider.Settings.Secure;
@@ -64,6 +64,7 @@
import com.android.internal.util.Preconditions;
import com.android.settingslib.Utils;
+import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.CutoutDecorProviderFactory;
@@ -75,7 +76,9 @@
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.qs.SettingObserver;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.tuner.TunerService;
@@ -119,8 +122,20 @@
R.id.display_cutout_right,
R.id.display_cutout_bottom
};
+ private final ScreenDecorationsLogger mLogger;
- private DisplayManager mDisplayManager;
+ private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
+ @Override
+ public void onFaceSensorLocationChanged() {
+ mLogger.onSensorLocationChanged();
+ if (mExecutor != null) {
+ mExecutor.execute(
+ () -> updateOverlayProviderViews(new Integer[]{mFaceScanningViewId}));
+ }
+ }
+ };
+
+ private DisplayTracker mDisplayTracker;
@VisibleForTesting
protected boolean mIsRegistered;
private final Context mContext;
@@ -128,7 +143,7 @@
private final TunerService mTunerService;
private final SecureSettings mSecureSettings;
@VisibleForTesting
- DisplayManager.DisplayListener mDisplayListener;
+ DisplayTracker.Callback mDisplayListener;
private CameraAvailabilityListener mCameraListener;
private final UserTracker mUserTracker;
private final PrivacyDotViewController mDotViewController;
@@ -152,6 +167,7 @@
private WindowManager mWindowManager;
private int mRotation;
private SettingObserver mColorInversionSetting;
+ @Nullable
private DelayableExecutor mExecutor;
private Handler mHandler;
boolean mPendingConfigChange;
@@ -171,6 +187,7 @@
DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
mFaceScanningViewId);
if (overlay != null) {
+ mLogger.cameraProtectionBoundsForScanningOverlay(bounds);
overlay.setProtection(protectionPath, bounds);
overlay.enableShowProtection(true);
updateOverlayWindowVisibilityIfViewExists(
@@ -183,6 +200,7 @@
}
if (mScreenDecorHwcLayer != null) {
+ mLogger.hwcLayerCameraProtectionBounds(bounds);
mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
mScreenDecorHwcLayer.enableShowProtection(true);
return;
@@ -196,11 +214,12 @@
}
++setProtectionCnt;
final DisplayCutoutView dcv = (DisplayCutoutView) view;
+ mLogger.dcvCameraBounds(id, bounds);
dcv.setProtection(protectionPath, bounds);
dcv.enableShowProtection(true);
}
if (setProtectionCnt == 0) {
- Log.e(TAG, "CutoutView not initialized showCameraProtection");
+ mLogger.cutoutViewNotInitialized();
}
}
@@ -302,20 +321,26 @@
SecureSettings secureSettings,
TunerService tunerService,
UserTracker userTracker,
+ DisplayTracker displayTracker,
PrivacyDotViewController dotViewController,
ThreadFactory threadFactory,
PrivacyDotDecorProviderFactory dotFactory,
- FaceScanningProviderFactory faceScanningFactory) {
+ FaceScanningProviderFactory faceScanningFactory,
+ ScreenDecorationsLogger logger,
+ AuthController authController) {
mContext = context;
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
mTunerService = tunerService;
mUserTracker = userTracker;
+ mDisplayTracker = displayTracker;
mDotViewController = dotViewController;
mThreadFactory = threadFactory;
mDotFactory = dotFactory;
mFaceScanningFactory = faceScanningFactory;
mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
+ mLogger = logger;
+ authController.addCallback(mAuthControllerCallback);
}
@Override
@@ -376,7 +401,6 @@
private void startOnScreenDecorationsThread() {
Trace.beginSection("ScreenDecorations#startOnScreenDecorationsThread");
mWindowManager = mContext.getSystemService(WindowManager.class);
- mDisplayManager = mContext.getSystemService(DisplayManager.class);
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
mRotation = mDisplayInfo.rotation;
mDisplayMode = mDisplayInfo.getMode();
@@ -393,17 +417,7 @@
setupDecorations();
setupCameraListener();
- mDisplayListener = new DisplayManager.DisplayListener() {
- @Override
- public void onDisplayAdded(int displayId) {
- // do nothing
- }
-
- @Override
- public void onDisplayRemoved(int displayId) {
- // do nothing
- }
-
+ mDisplayListener = new DisplayTracker.Callback() {
@Override
public void onDisplayChanged(int displayId) {
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
@@ -474,8 +488,7 @@
}
}
};
-
- mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
+ mDisplayTracker.addDisplayChangeCallback(mDisplayListener, new HandlerExecutor(mHandler));
updateConfiguration();
Trace.endSection();
}
@@ -1315,7 +1328,7 @@
if (showProtection) {
// Make sure that our measured height encompasses the protection
- mTotalBounds.union(mBoundingRect);
+ mTotalBounds.set(mBoundingRect);
mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top,
(int) protectionRect.right, (int) protectionRect.bottom);
setMeasuredDimension(
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 0fc9ef9..632fcdc 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -22,8 +22,6 @@
import android.os.HandlerThread;
import android.util.Log;
-import androidx.annotation.Nullable;
-
import com.android.systemui.dagger.GlobalRootComponent;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dagger.WMComponent;
@@ -55,7 +53,6 @@
mContext = context;
}
- @Nullable
protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder();
/**
@@ -72,11 +69,6 @@
* Starts the initialization process. This stands up the Dagger graph.
*/
public void init(boolean fromTest) throws ExecutionException, InterruptedException {
- GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder();
- if (globalBuilder == null) {
- return;
- }
-
mRootComponent = getGlobalRootComponentBuilder()
.context(mContext)
.instrumentationTest(fromTest)
@@ -127,7 +119,6 @@
.setBackAnimation(Optional.ofNullable(null))
.setDesktopMode(Optional.ofNullable(null));
}
-
mSysUIComponent = builder.build();
if (initializeComponents) {
mSysUIComponent.init();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
index 55c095b..8aa3040 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
@@ -16,7 +16,6 @@
package com.android.systemui
-import android.app.Application
import android.content.Context
import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent
import com.android.systemui.dagger.GlobalRootComponent
@@ -25,17 +24,7 @@
* {@link SystemUIInitializer} that stands up AOSP SystemUI.
*/
class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) {
-
- override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? {
- return when (Application.getProcessName()) {
- SCREENSHOT_CROSS_PROFILE_PROCESS -> null
- else -> DaggerReferenceGlobalRootComponent.builder()
- }
- }
-
- companion object {
- private const val SYSTEMUI_PROCESS = "com.android.systemui"
- private const val SCREENSHOT_CROSS_PROFILE_PROCESS =
- "$SYSTEMUI_PROCESS:screenshot_cross_profile"
+ override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder {
+ return DaggerReferenceGlobalRootComponent.builder()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
index fbb909f..2c97d62 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
@@ -27,6 +27,7 @@
import android.util.Log;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -67,8 +68,8 @@
}
@Inject
- public AccessibilityButtonModeObserver(Context context) {
- super(context, Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
+ public AccessibilityButtonModeObserver(Context context, UserTracker userTracker) {
+ super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
index b32ebcc..53a21b3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
@@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
import javax.inject.Inject;
@@ -48,8 +49,8 @@
}
@Inject
- public AccessibilityButtonTargetsObserver(Context context) {
- super(context, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+ public AccessibilityButtonTargetsObserver(Context context, UserTracker userTracker) {
+ super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
index e4e0da6..326773f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.settings.UserTracker;
import java.util.ArrayList;
import java.util.List;
@@ -44,6 +45,7 @@
public abstract class SecureSettingsContentObserver<T> {
private final ContentResolver mContentResolver;
+ private final UserTracker mUserTracker;
@VisibleForTesting
final ContentObserver mContentObserver;
@@ -52,9 +54,11 @@
@VisibleForTesting
final List<T> mListeners = new ArrayList<>();
- protected SecureSettingsContentObserver(Context context, String secureSettingsKey) {
+ protected SecureSettingsContentObserver(Context context, UserTracker userTracker,
+ String secureSettingsKey) {
mKey = secureSettingsKey;
mContentResolver = context.getContentResolver();
+ mUserTracker = userTracker;
mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
@@ -103,7 +107,7 @@
* See {@link Settings.Secure}.
*/
public final String getSettingsValue() {
- return Settings.Secure.getStringForUser(mContentResolver, mKey, UserHandle.USER_CURRENT);
+ return Settings.Secure.getStringForUser(mContentResolver, mKey, mUserTracker.getUserId());
}
private void updateValueChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index dab73e9..ddac25b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -35,14 +35,11 @@
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.os.UserHandle;
import android.util.Log;
-import android.view.Display;
import android.view.IWindowManager;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
-import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityManager;
@@ -52,6 +49,8 @@
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -179,7 +178,9 @@
private final SystemActionsBroadcastReceiver mReceiver;
private final Context mContext;
+ private final UserTracker mUserTracker;
private final Optional<Recents> mRecentsOptional;
+ private final DisplayTracker mDisplayTracker;
private Locale mLocale;
private final AccessibilityManager mA11yManager;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -190,13 +191,17 @@
@Inject
public SystemActions(Context context,
+ UserTracker userTracker,
NotificationShadeWindowController notificationShadeController,
ShadeController shadeController,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
- Optional<Recents> recentsOptional) {
+ Optional<Recents> recentsOptional,
+ DisplayTracker displayTracker) {
mContext = context;
+ mUserTracker = userTracker;
mShadeController = shadeController;
mRecentsOptional = recentsOptional;
+ mDisplayTracker = displayTracker;
mReceiver = new SystemActionsBroadcastReceiver();
mLocale = mContext.getResources().getConfiguration().getLocales().get(0);
mA11yManager = (AccessibilityManager) mContext.getSystemService(
@@ -205,7 +210,8 @@
// Saving in instance variable since to prevent GC since
// NotificationShadeWindowController.registerCallback() only keeps weak references.
mNotificationShadeCallback =
- (keyguardShowing, keyguardOccluded, bouncerShowing, mDozing, panelExpanded) ->
+ (keyguardShowing, keyguardOccluded, bouncerShowing, mDozing, panelExpanded,
+ isDreaming) ->
registerOrUnregisterDismissNotificationShadeAction();
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
}
@@ -343,6 +349,7 @@
/**
* Register a system action.
+ *
* @param actionId the action ID to register.
*/
public void register(int actionId) {
@@ -440,6 +447,7 @@
/**
* Unregister a system action.
+ *
* @param actionId the action ID to unregister.
*/
public void unregister(int actionId) {
@@ -475,7 +483,8 @@
}
private void handleNotifications() {
- mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::animateExpandNotificationsPanel);
+ mCentralSurfacesOptionalLazy.get().ifPresent(
+ CentralSurfaces::animateExpandNotificationsPanel);
}
private void handleQuickSettings() {
@@ -507,7 +516,7 @@
private void handleTakeScreenshot() {
ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
- screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ screenshotHelper.takeScreenshot(
SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null);
}
@@ -517,7 +526,7 @@
private void handleAccessibilityButton() {
AccessibilityManager.getInstance(mContext).notifyAccessibilityButtonClicked(
- Display.DEFAULT_DISPLAY);
+ mDisplayTracker.getDefaultDisplayId());
}
private void handleAccessibilityButtonChooser() {
@@ -525,7 +534,7 @@
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final String chooserClassName = AccessibilityButtonChooserActivity.class.getName();
intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
- mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
}
private void handleAccessibilityShortcut() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index ab11fce..b3574bf 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -39,6 +39,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.model.SysUiState;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import java.io.PrintWriter;
@@ -62,6 +63,7 @@
private final AccessibilityManager mAccessibilityManager;
private final CommandQueue mCommandQueue;
private final OverviewProxyService mOverviewProxyService;
+ private final DisplayTracker mDisplayTracker;
private WindowMagnificationConnectionImpl mWindowMagnificationConnectionImpl;
private SysUiState mSysUiState;
@@ -102,7 +104,8 @@
@Inject
public WindowMagnification(Context context, @Main Handler mainHandler,
CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
- SysUiState sysUiState, OverviewProxyService overviewProxyService) {
+ SysUiState sysUiState, OverviewProxyService overviewProxyService,
+ DisplayTracker displayTracker) {
mContext = context;
mHandler = mainHandler;
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
@@ -110,6 +113,7 @@
mModeSwitchesController = modeSwitchesController;
mSysUiState = sysUiState;
mOverviewProxyService = overviewProxyService;
+ mDisplayTracker = displayTracker;
mMagnificationControllerSupplier = new ControllerSupplier(context,
mHandler, this, context.getSystemService(DisplayManager.class), sysUiState);
}
@@ -130,14 +134,14 @@
private void updateSysUiStateFlag() {
//TODO(b/187510533): support multi-display once SysuiState supports it.
final WindowMagnificationController controller =
- mMagnificationControllerSupplier.valueAt(Display.DEFAULT_DISPLAY);
+ mMagnificationControllerSupplier.valueAt(mDisplayTracker.getDefaultDisplayId());
if (controller != null) {
controller.updateSysUIStateFlag();
} else {
// The instance is initialized when there is an IPC request. Considering
// self-crash cases, we need to reset the flag in such situation.
mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, false)
- .commitUpdate(Display.DEFAULT_DISPLAY);
+ .commitUpdate(mDisplayTracker.getDefaultDisplayId());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
index 9af8300..7441e03 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
@@ -43,6 +43,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Prefs;
import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.util.settings.SecureSettings;
import java.util.List;
@@ -60,6 +61,7 @@
private static final float DEFAULT_POSITION_Y_PERCENT = 0.9f;
private final Context mContext;
+ private final SecureSettings mSecureSettings;
private final AccessibilityFloatingMenuView mMenuView;
private final MigrationTooltipView mMigrationTooltipView;
private final DockTooltipView mDockTooltipView;
@@ -77,7 +79,7 @@
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
- mMenuView.setSizeType(getSizeType(mContext));
+ mMenuView.setSizeType(getSizeType());
}
};
@@ -85,8 +87,8 @@
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
- mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
- getOpacityValue(mContext));
+ mMenuView.updateOpacityWith(isFadeEffectEnabled(),
+ getOpacityValue());
}
};
@@ -98,16 +100,19 @@
}
};
- public AccessibilityFloatingMenu(Context context) {
+ public AccessibilityFloatingMenu(Context context, SecureSettings secureSettings) {
mContext = context;
+ mSecureSettings = secureSettings;
mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context));
mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
mDockTooltipView = new DockTooltipView(mContext, mMenuView);
}
@VisibleForTesting
- AccessibilityFloatingMenu(Context context, AccessibilityFloatingMenuView menuView) {
+ AccessibilityFloatingMenu(Context context, SecureSettings secureSettings,
+ AccessibilityFloatingMenuView menuView) {
mContext = context;
+ mSecureSettings = secureSettings;
mMenuView = menuView;
mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
mDockTooltipView = new DockTooltipView(mContext, mMenuView);
@@ -130,10 +135,10 @@
mMenuView.show();
mMenuView.onTargetsChanged(targetList);
- mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
- getOpacityValue(mContext));
- mMenuView.setSizeType(getSizeType(mContext));
- mMenuView.setShapeType(getShapeType(mContext));
+ mMenuView.updateOpacityWith(isFadeEffectEnabled(),
+ getOpacityValue());
+ mMenuView.setSizeType(getSizeType());
+ mMenuView.setShapeType(getShapeType());
mMenuView.setOnDragEndListener(this::onDragEnd);
showMigrationTooltipIfNecessary();
@@ -170,17 +175,17 @@
// Migration tooltip was the android S feature. It's just used on the Android version from R
// to S. In addition, it only shows once.
private void showMigrationTooltipIfNecessary() {
- if (isMigrationTooltipPromptEnabled(mContext)) {
+ if (isMigrationTooltipPromptEnabled()) {
mMigrationTooltipView.show();
- Settings.Secure.putInt(mContext.getContentResolver(),
+ mSecureSettings.putInt(
ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT, /* disabled */ 0);
}
}
- private static boolean isMigrationTooltipPromptEnabled(Context context) {
- return Settings.Secure.getInt(
- context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
+ private boolean isMigrationTooltipPromptEnabled() {
+ return mSecureSettings.getInt(
+ ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1;
}
@@ -212,57 +217,61 @@
}
}
- private static boolean isFadeEffectEnabled(Context context) {
- return Settings.Secure.getInt(
- context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
+ private boolean isFadeEffectEnabled() {
+ return mSecureSettings.getInt(
+ ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
DEFAULT_FADE_EFFECT_IS_ENABLED) == /* enabled */ 1;
}
- private static float getOpacityValue(Context context) {
- return Settings.Secure.getFloat(
- context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_OPACITY,
+ private float getOpacityValue() {
+ return mSecureSettings.getFloat(
+ ACCESSIBILITY_FLOATING_MENU_OPACITY,
DEFAULT_OPACITY_VALUE);
}
- private static int getSizeType(Context context) {
- return Settings.Secure.getInt(
- context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
+ private int getSizeType() {
+ return mSecureSettings.getInt(
+ ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
}
- private static int getShapeType(Context context) {
- return Settings.Secure.getInt(
- context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
+ private int getShapeType() {
+ return mSecureSettings.getInt(
+ ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
ShapeType.OVAL);
}
private void registerContentObservers() {
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
/* notifyForDescendants */ false, mContentObserver,
UserHandle.USER_CURRENT);
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+ /* notifyForDescendants */ false, mContentObserver,
+ UserHandle.USER_CURRENT);
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE,
/* notifyForDescendants */ false, mSizeContentObserver,
UserHandle.USER_CURRENT);
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
/* notifyForDescendants */ false, mFadeOutContentObserver,
UserHandle.USER_CURRENT);
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY),
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY,
/* notifyForDescendants */ false, mFadeOutContentObserver,
UserHandle.USER_CURRENT);
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES),
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
/* notifyForDescendants */ false,
mEnabledA11yServicesContentObserver, UserHandle.USER_CURRENT);
}
private void unregisterContentObservers() {
- mContext.getContentResolver().unregisterContentObserver(mContentObserver);
- mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver);
- mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver);
- mContext.getContentResolver().unregisterContentObserver(
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ mSecureSettings.unregisterContentObserver(mSizeContentObserver);
+ mSecureSettings.unregisterContentObserver(mFadeOutContentObserver);
+ mSecureSettings.unregisterContentObserver(
mEnabledA11yServicesContentObserver);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 403941f..6216b89 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -31,6 +31,7 @@
import com.android.systemui.accessibility.AccessibilityButtonModeObserver.AccessibilityButtonMode;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.util.settings.SecureSettings;
import javax.inject.Inject;
@@ -44,6 +45,7 @@
private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final SecureSettings mSecureSettings;
private Context mContext;
@VisibleForTesting
@@ -85,11 +87,13 @@
public AccessibilityFloatingMenuController(Context context,
AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
AccessibilityButtonModeObserver accessibilityButtonModeObserver,
- KeyguardUpdateMonitor keyguardUpdateMonitor) {
+ KeyguardUpdateMonitor keyguardUpdateMonitor,
+ SecureSettings secureSettings) {
mContext = context;
mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver;
mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ mSecureSettings = secureSettings;
mIsKeyguardVisible = false;
}
@@ -159,7 +163,7 @@
private void showFloatingMenu() {
if (mFloatingMenu == null) {
- mFloatingMenu = new AccessibilityFloatingMenu(mContext);
+ mFloatingMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings);
}
mFloatingMenu.show();
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 7c2673c..1ea173e 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -1,7 +1,5 @@
package com.android.systemui.assist;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import static com.android.systemui.DejankUtils.whitelistIpcs;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
@@ -35,8 +33,11 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.model.SysUiState;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.util.settings.SecureSettings;
import javax.inject.Inject;
@@ -119,6 +120,9 @@
private final UiController mUiController;
protected final Lazy<SysUiState> mSysUiState;
protected final AssistLogger mAssistLogger;
+ private final UserTracker mUserTracker;
+ private final DisplayTracker mDisplayTracker;
+ private final SecureSettings mSecureSettings;
private final DeviceProvisionedController mDeviceProvisionedController;
private final CommandQueue mCommandQueue;
@@ -135,7 +139,10 @@
Lazy<SysUiState> sysUiState,
DefaultUiController defaultUiController,
AssistLogger assistLogger,
- @Main Handler uiHandler) {
+ @Main Handler uiHandler,
+ UserTracker userTracker,
+ DisplayTracker displayTracker,
+ SecureSettings secureSettings) {
mContext = context;
mDeviceProvisionedController = controller;
mCommandQueue = commandQueue;
@@ -143,6 +150,9 @@
mAssistDisclosure = new AssistDisclosure(context, uiHandler);
mPhoneStateMonitor = phoneStateMonitor;
mAssistLogger = assistLogger;
+ mUserTracker = userTracker;
+ mDisplayTracker = displayTracker;
+ mSecureSettings = secureSettings;
registerVoiceInteractionSessionListener();
@@ -206,7 +216,7 @@
.setFlag(
SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED,
hints.getBoolean(CONSTRAINED_KEY, false))
- .commitUpdate(DEFAULT_DISPLAY);
+ .commitUpdate(mDisplayTracker.getDefaultDisplayId());
}
}
});
@@ -273,7 +283,7 @@
CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
false /* force */);
- boolean structureEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ boolean structureEnabled = mSecureSettings.getIntForUser(
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
final SearchManager searchManager =
@@ -300,7 +310,7 @@
@Override
public void run() {
mContext.startActivityAsUser(intent, opts.toBundle(),
- new UserHandle(UserHandle.USER_CURRENT));
+ mUserTracker.getUserHandle());
}
});
} catch (ActivityNotFoundException e) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
index e4c197f..1404053 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
@@ -35,13 +35,13 @@
override val actsAsConfirmButton: Boolean = true
- override fun shouldAnimateForTransition(
+ override fun shouldAnimateIconViewForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
): Boolean = when (newState) {
STATE_PENDING_CONFIRMATION -> true
STATE_AUTHENTICATED -> false
- else -> super.shouldAnimateForTransition(oldState, newState)
+ else -> super.shouldAnimateIconViewForTransition(oldState, newState)
}
@RawRes
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index b962cc4..436f9df 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -104,12 +104,14 @@
iconView.frame = 0
iconViewOverlay.frame = 0
- if (shouldAnimateForTransition(lastState, newState)) {
- iconView.playAnimation()
- iconViewOverlay.playAnimation()
- } else if (lastState == STATE_IDLE && newState == STATE_AUTHENTICATING_ANIMATING_IN) {
+ if (shouldAnimateIconViewForTransition(lastState, newState)) {
iconView.playAnimation()
}
+
+ if (shouldAnimateIconViewOverlayForTransition(lastState, newState)) {
+ iconViewOverlay.playAnimation()
+ }
+
LottieColorUtils.applyDynamicColors(context, iconView)
LottieColorUtils.applyDynamicColors(context, iconViewOverlay)
}
@@ -127,7 +129,7 @@
}
iconView.frame = 0
- if (shouldAnimateForTransition(lastState, newState)) {
+ if (shouldAnimateIconViewForTransition(lastState, newState)) {
iconView.playAnimation()
}
LottieColorUtils.applyDynamicColors(context, iconView)
@@ -160,7 +162,20 @@
return if (id != null) context.getString(id) else null
}
- protected open fun shouldAnimateForTransition(
+ protected open fun shouldAnimateIconViewForTransition(
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
+ ) = when (newState) {
+ STATE_HELP,
+ STATE_ERROR -> true
+ STATE_AUTHENTICATING_ANIMATING_IN,
+ STATE_AUTHENTICATING ->
+ oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE
+ STATE_AUTHENTICATED -> true
+ else -> false
+ }
+
+ protected open fun shouldAnimateIconViewOverlayForTransition(
@BiometricState oldState: Int,
@BiometricState newState: Int
) = when (newState) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 68e1f72..febf75e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -847,7 +847,7 @@
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
windowFlags,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 092339a..2dc0cd3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -18,6 +18,7 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
@@ -76,6 +77,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.DozeReceiver;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.data.repository.BiometricType;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.VibratorHelper;
@@ -85,8 +87,10 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -150,6 +154,7 @@
@Nullable private List<FingerprintSensorPropertiesInternal> mUdfpsProps;
@Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
+ @NonNull private final Map<Integer, Boolean> mFpEnrolledForUser = new HashMap<>();
@NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
@NonNull private final SparseBooleanArray mSfpsEnrolledForUser;
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
@@ -161,7 +166,6 @@
private final @Background DelayableExecutor mBackgroundExecutor;
private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
-
private final VibratorHelper mVibratorHelper;
private void vibrateSuccess(int modality) {
@@ -331,27 +335,35 @@
mExecution.assertIsMainThread();
Log.d(TAG, "handleEnrollmentsChanged, userId: " + userId + ", sensorId: " + sensorId
+ ", hasEnrollments: " + hasEnrollments);
- if (mUdfpsProps == null) {
- Log.d(TAG, "handleEnrollmentsChanged, mUdfpsProps is null");
- } else {
- for (FingerprintSensorPropertiesInternal prop : mUdfpsProps) {
+ BiometricType sensorBiometricType = BiometricType.UNKNOWN;
+ if (mFpProps != null) {
+ for (FingerprintSensorPropertiesInternal prop: mFpProps) {
if (prop.sensorId == sensorId) {
- mUdfpsEnrolledForUser.put(userId, hasEnrollments);
+ mFpEnrolledForUser.put(userId, hasEnrollments);
+ if (prop.isAnyUdfpsType()) {
+ sensorBiometricType = BiometricType.UNDER_DISPLAY_FINGERPRINT;
+ mUdfpsEnrolledForUser.put(userId, hasEnrollments);
+ } else if (prop.isAnySidefpsType()) {
+ sensorBiometricType = BiometricType.SIDE_FINGERPRINT;
+ mSfpsEnrolledForUser.put(userId, hasEnrollments);
+ } else if (prop.sensorType == TYPE_REAR) {
+ sensorBiometricType = BiometricType.REAR_FINGERPRINT;
+ }
+ break;
}
}
}
-
- if (mSidefpsProps == null) {
- Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null");
- } else {
- for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) {
+ if (mFaceProps != null && sensorBiometricType == BiometricType.UNKNOWN) {
+ for (FaceSensorPropertiesInternal prop : mFaceProps) {
if (prop.sensorId == sensorId) {
- mSfpsEnrolledForUser.put(userId, hasEnrollments);
+ sensorBiometricType = BiometricType.FACE;
+ break;
}
}
}
for (Callback cb : mCallbacks) {
cb.onEnrollmentsChanged();
+ cb.onEnrollmentsChanged(sensorBiometricType, userId, hasEnrollments);
}
}
@@ -604,6 +616,11 @@
}
}
+ /** Get FP sensor properties */
+ public @Nullable List<FingerprintSensorPropertiesInternal> getFingerprintProperties() {
+ return mFpProps;
+ }
+
/**
* @return where the face sensor exists in pixels in the current device orientation. Returns
* null if no face sensor exists.
@@ -828,7 +845,7 @@
}
@Override
- public void setBiometicContextListener(IBiometricContextListener listener) {
+ public void setBiometricContextListener(IBiometricContextListener listener) {
mBiometricContextListener = listener;
notifyDozeChanged(mStatusBarStateController.isDozing(),
mWakefulnessLifecycle.getWakefulness());
@@ -1081,6 +1098,13 @@
return mSfpsEnrolledForUser.get(userId);
}
+ /**
+ * Whether the passed userId has enrolled at least one fingerprint.
+ */
+ public boolean isFingerprintEnrolled(int userId) {
+ return mFpEnrolledForUser.getOrDefault(userId, false);
+ }
+
private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
mCurrentDialogArgs = args;
@@ -1263,6 +1287,16 @@
default void onEnrollmentsChanged() {}
/**
+ * Called when UDFPS enrollments have changed. This is called after boot and on changes to
+ * enrollment.
+ */
+ default void onEnrollmentsChanged(
+ @NonNull BiometricType biometricType,
+ int userId,
+ boolean hasEnrollments
+ ) {}
+
+ /**
* Called when the biometric prompt starts showing.
*/
default void onBiometricPromptShown() {}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
index 4b57d45..53ab6d6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
@@ -55,11 +55,11 @@
private val fadeDuration = 83L
private val retractDuration = 400L
private var alphaInDuration: Long = 0
- private var unlockedRippleInProgress: Boolean = false
private val dwellShader = DwellRippleShader()
private val dwellPaint = Paint()
private val rippleShader = RippleShader()
private val ripplePaint = Paint()
+ private var unlockedRippleAnimator: AnimatorSet? = null
private var fadeDwellAnimator: Animator? = null
private var retractDwellAnimator: Animator? = null
private var dwellPulseOutAnimator: Animator? = null
@@ -86,7 +86,7 @@
init {
rippleShader.color = 0xffffffff.toInt() // default color
- rippleShader.progress = 0f
+ rippleShader.rawProgress = 0f
rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
ripplePaint.shader = rippleShader
@@ -205,7 +205,7 @@
* Plays a ripple animation that grows to the dwellRadius with distortion.
*/
fun startDwellRipple(isDozing: Boolean) {
- if (unlockedRippleInProgress || dwellPulseOutAnimator?.isRunning == true) {
+ if (unlockedRippleAnimator?.isRunning == true || dwellPulseOutAnimator?.isRunning == true) {
return
}
@@ -262,16 +262,14 @@
* Ripple that bursts outwards from the position of the sensor to the edges of the screen
*/
fun startUnlockedRipple(onAnimationEnd: Runnable?) {
- if (unlockedRippleInProgress) {
- return // Ignore if ripple effect is already playing
- }
+ unlockedRippleAnimator?.cancel()
val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = AuthRippleController.RIPPLE_ANIMATION_DURATION
addUpdateListener { animator ->
val now = animator.currentPlayTime
- rippleShader.progress = animator.animatedValue as Float
+ rippleShader.rawProgress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
@@ -289,14 +287,13 @@
}
}
- val animatorSet = AnimatorSet().apply {
+ unlockedRippleAnimator = AnimatorSet().apply {
playTogether(
rippleAnimator,
alphaInAnimator
)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
- unlockedRippleInProgress = true
rippleShader.rippleFill = false
drawRipple = true
visibility = VISIBLE
@@ -304,13 +301,13 @@
override fun onAnimationEnd(animation: Animator?) {
onAnimationEnd?.run()
- unlockedRippleInProgress = false
drawRipple = false
visibility = GONE
+ unlockedRippleAnimator = null
}
})
}
- animatorSet.start()
+ unlockedRippleAnimator?.start()
}
fun resetRippleAlpha() {
@@ -345,7 +342,7 @@
override fun onDraw(canvas: Canvas?) {
// To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
// the active effect area. Values here should be kept in sync with the
- // animation implementation in the ripple shader.
+ // animation implementation in the ripple shader. (Twice bigger)
if (drawDwell) {
val maskRadius = (1 - (1 - dwellShader.progress) * (1 - dwellShader.progress) *
(1 - dwellShader.progress)) * dwellRadius * 2f
@@ -354,10 +351,8 @@
}
if (drawRipple) {
- val mask = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * radius * 2f
canvas?.drawCircle(origin.x.toFloat(), origin.y.toFloat(),
- mask, ripplePaint)
+ rippleShader.currentWidth, ripplePaint)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java
new file mode 100644
index 0000000..902bb18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/** Provides the status of the interactive to auth feature. */
+public interface FingerprintInteractiveToAuthProvider {
+ /**
+ *
+ * @param userId the user Id.
+ * @return true if the InteractiveToAuthFeature is enabled, false if disabled.
+ */
+ boolean isEnabled(int userId);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 1afa9b2..c799e91 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -54,12 +54,18 @@
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.PrintWriter
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
private const val TAG = "SideFpsController"
@@ -79,6 +85,9 @@
displayManager: DisplayManager,
@Main private val mainExecutor: DelayableExecutor,
@Main private val handler: Handler,
+ private val alternateBouncerInteractor: AlternateBouncerInteractor,
+ @Application private val scope: CoroutineScope,
+ private val featureFlags: FeatureFlags,
dumpManager: DumpManager
) : Dumpable {
val requests: HashSet<SideFpsUiRequestSource> = HashSet()
@@ -111,7 +120,7 @@
context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
private val isReverseDefaultRotation =
- context.getResources().getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
+ context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
private var overlayHideAnimator: ViewPropertyAnimator? = null
@@ -168,9 +177,26 @@
}
)
overviewProxyService.addCallback(overviewProxyListener)
+ listenForAlternateBouncerVisibility()
+
dumpManager.registerDumpable(this)
}
+ private fun listenForAlternateBouncerVisibility() {
+ alternateBouncerInteractor.setAlternateBouncerUIAvailable(true)
+ if (featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)) {
+ scope.launch {
+ alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
+ if (isVisible) {
+ show(SideFpsUiRequestSource.ALTERNATE_BOUNCER)
+ } else {
+ hide(SideFpsUiRequestSource.ALTERNATE_BOUNCER)
+ }
+ }
+ }
+ }
+ }
+
/** Shows the side fps overlay if not already shown. */
fun show(request: SideFpsUiRequestSource) {
requests.add(request)
@@ -268,10 +294,12 @@
val isDefaultOrientation =
if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
val size = windowManager.maximumWindowMetrics.bounds
+
val displayWidth = if (isDefaultOrientation) size.width() else size.height()
val displayHeight = if (isDefaultOrientation) size.height() else size.width()
val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
+
val sensorBounds =
if (overlayOffsets.isYAligned()) {
Rect(
@@ -297,6 +325,7 @@
overlayViewParams.x = sensorBounds.left
overlayViewParams.y = sensorBounds.top
+
windowManager.updateViewLayout(overlayView, overlayViewParams)
}
@@ -306,7 +335,12 @@
}
// hide after a few seconds if the sensor is oriented down and there are
// large overlapping system bars
- val rotation = context.display?.rotation
+ var rotation = context.display?.rotation
+
+ if (rotation != null) {
+ rotation = getRotationFromDefault(rotation)
+ }
+
if (
windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar() &&
((rotation == Surface.ROTATION_270 && overlayOffsets.isYAligned()) ||
@@ -415,4 +449,5 @@
AUTO_SHOW,
/** Pin, pattern or password bouncer */
PRIMARY_BOUNCER,
+ ALTERNATE_BOUNCER
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 4130cf5..ef7dcb7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -190,11 +190,6 @@
open fun listenForTouchesOutsideView(): Boolean = false
/**
- * Called on touches outside of the view if listenForTouchesOutsideView returns true
- */
- open fun onTouchOutsideView() {}
-
- /**
* Called when a view should announce an accessibility event.
*/
open fun doAnnounceForAccessibility(str: String) {}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index f3136ba..3fd00ae 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -73,6 +73,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -86,6 +87,7 @@
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.Execution;
+import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.SystemClock;
import java.io.PrintWriter;
@@ -149,6 +151,8 @@
@NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
@NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
@Nullable private final TouchProcessor mTouchProcessor;
+ @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor;
+ @NonNull private final SecureSettings mSecureSettings;
// Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
// sensors, this, in addition to a lot of the code here, will be updated.
@@ -220,6 +224,10 @@
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("mSensorProps=(" + mSensorProps + ")");
+ pw.println("Using new touch detection framework: " + mFeatureFlags.isEnabled(
+ Flags.UDFPS_NEW_TOUCH_DETECTION));
+ pw.println("Using ellipse touch detection: " + mFeatureFlags.isEnabled(
+ Flags.UDFPS_ELLIPSE_DETECTION));
}
public class UdfpsOverlayController extends IUdfpsOverlayController.Stub {
@@ -232,12 +240,12 @@
mShadeExpansionStateManager, mKeyguardViewManager,
mKeyguardUpdateMonitor, mDialogManager, mDumpManager,
mLockscreenShadeTransitionController, mConfigurationController,
- mSystemClock, mKeyguardStateController,
+ mKeyguardStateController,
mUnlockedScreenOffAnimationController,
- mUdfpsDisplayMode, requestId, reason, callback,
+ mUdfpsDisplayMode, mSecureSettings, requestId, reason, callback,
(view, event, fromUdfpsView) -> onTouch(requestId, event,
fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
- mPrimaryBouncerInteractor)));
+ mPrimaryBouncerInteractor, mAlternateBouncerInteractor)));
}
@Override
@@ -329,13 +337,13 @@
if (!mOverlayParams.equals(overlayParams)) {
mOverlayParams = overlayParams;
- final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer();
+ final boolean wasShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState();
// When the bounds change it's always necessary to re-create the overlay's window with
// new LayoutParams. If the overlay needs to be shown, this will re-create and show the
// overlay with the updated LayoutParams. Otherwise, the overlay will remain hidden.
redrawOverlay();
- if (wasShowingAltAuth) {
+ if (wasShowingAlternateBouncer) {
mKeyguardViewManager.showBouncer(true);
}
}
@@ -543,9 +551,6 @@
final UdfpsView udfpsView = mOverlay.getOverlayView();
boolean handled = false;
switch (event.getActionMasked()) {
- case MotionEvent.ACTION_OUTSIDE:
- udfpsView.onTouchOutsideView();
- return true;
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_HOVER_ENTER:
Trace.beginSection("UdfpsController.onTouch.ACTION_DOWN");
@@ -719,7 +724,9 @@
@NonNull Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider,
@NonNull @BiometricsBackground Executor biometricsExecutor,
@NonNull PrimaryBouncerInteractor primaryBouncerInteractor,
- @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) {
+ @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor,
+ @NonNull AlternateBouncerInteractor alternateBouncerInteractor,
+ @NonNull SecureSettings secureSettings) {
mContext = context;
mExecution = execution;
mVibrator = vibrator;
@@ -759,6 +766,8 @@
mBiometricExecutor = biometricsExecutor;
mPrimaryBouncerInteractor = primaryBouncerInteractor;
+ mAlternateBouncerInteractor = alternateBouncerInteractor;
+ mSecureSettings = secureSettings;
mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
? singlePointerTouchProcessor : null;
@@ -853,9 +862,7 @@
onFingerUp(mOverlay.getRequestId(), oldView);
}
final boolean removed = mOverlay.hide();
- if (mKeyguardViewManager.isShowingAlternateBouncer()) {
- mKeyguardViewManager.hideAlternateBouncer(true);
- }
+ mKeyguardViewManager.hideAlternateBouncer(true);
Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed);
} else {
Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden");
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 8db4927..b6b5d26 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -50,6 +50,7 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionStateManager
@@ -59,7 +60,7 @@
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.settings.SecureSettings
private const val TAG = "UdfpsControllerOverlay"
@@ -86,10 +87,10 @@
private val dumpManager: DumpManager,
private val transitionController: LockscreenShadeTransitionController,
private val configurationController: ConfigurationController,
- private val systemClock: SystemClock,
private val keyguardStateController: KeyguardStateController,
private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
+ private val secureSettings: SecureSettings,
val requestId: Long,
@ShowReason val requestReason: Int,
private val controllerCallback: IUdfpsOverlayControllerCallback,
@@ -97,7 +98,8 @@
private val activityLaunchAnimator: ActivityLaunchAnimator,
private val featureFlags: FeatureFlags,
private val primaryBouncerInteractor: PrimaryBouncerInteractor,
- private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
+ private val alternateBouncerInteractor: AlternateBouncerInteractor,
+ private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
) {
/** The view, when [isShowing], or null. */
var overlayView: UdfpsView? = null
@@ -131,7 +133,7 @@
/** A helper if the [requestReason] was due to enrollment. */
val enrollHelper: UdfpsEnrollHelper? =
if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) {
- UdfpsEnrollHelper(context, fingerprintManager, requestReason)
+ UdfpsEnrollHelper(context, fingerprintManager, secureSettings, requestReason)
} else {
null
}
@@ -255,14 +257,14 @@
dumpManager,
transitionController,
configurationController,
- systemClock,
keyguardStateController,
unlockedScreenOffAnimationController,
dialogManager,
controller,
activityLaunchAnimator,
featureFlags,
- primaryBouncerInteractor
+ primaryBouncerInteractor,
+ alternateBouncerInteractor,
)
}
REASON_AUTH_BP -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
index d5c763d3..cfa8ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -24,11 +24,12 @@
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.UserHandle;
-import android.provider.Settings;
import android.util.Log;
import android.util.TypedValue;
import android.view.accessibility.AccessibilityManager;
+import com.android.systemui.util.settings.SecureSettings;
+
import java.util.ArrayList;
import java.util.List;
@@ -51,8 +52,8 @@
void onLastStepAcquired();
}
- @NonNull private final Context mContext;
@NonNull private final FingerprintManager mFingerprintManager;
+ @NonNull private final SecureSettings mSecureSettings;
// IUdfpsOverlayController reason
private final int mEnrollReason;
private final boolean mAccessibilityEnabled;
@@ -70,10 +71,11 @@
@Nullable Listener mListener;
public UdfpsEnrollHelper(@NonNull Context context,
- @NonNull FingerprintManager fingerprintManager, int reason) {
+ @NonNull FingerprintManager fingerprintManager, SecureSettings secureSettings,
+ int reason) {
- mContext = context;
mFingerprintManager = fingerprintManager;
+ mSecureSettings = secureSettings;
mEnrollReason = reason;
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
@@ -84,8 +86,7 @@
// Number of pixels per mm
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
context.getResources().getDisplayMetrics());
- boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(),
- NEW_COORDS_OVERRIDE, 0,
+ boolean useNewCoords = mSecureSettings.getIntForUser(NEW_COORDS_OVERRIDE, 0,
UserHandle.USER_CURRENT) != 0;
if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) {
Log.v(TAG, "Using new coordinates");
@@ -210,8 +211,7 @@
float scale = SCALE;
if (Build.IS_ENG || Build.IS_USERDEBUG) {
- scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
- SCALE_OVERRIDE, SCALE,
+ scale = mSecureSettings.getFloatForUser(SCALE_OVERRIDE, SCALE,
UserHandle.USER_CURRENT);
}
final int index = mLocationsEnrolled - mCenterTouchCount;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
index 63144fc..addbee9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
@@ -31,6 +31,7 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -39,16 +40,14 @@
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
-import com.android.systemui.statusbar.phone.KeyguardBouncer
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateBouncer
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -65,25 +64,26 @@
dumpManager: DumpManager,
private val lockScreenShadeTransitionController: LockscreenShadeTransitionController,
private val configurationController: ConfigurationController,
- private val systemClock: SystemClock,
private val keyguardStateController: KeyguardStateController,
private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
systemUIDialogManager: SystemUIDialogManager,
private val udfpsController: UdfpsController,
private val activityLaunchAnimator: ActivityLaunchAnimator,
featureFlags: FeatureFlags,
- private val primaryBouncerInteractor: PrimaryBouncerInteractor
+ private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+ private val alternateBouncerInteractor: AlternateBouncerInteractor,
) :
UdfpsAnimationViewController<UdfpsKeyguardView>(
view,
statusBarStateController,
shadeExpansionStateManager,
systemUIDialogManager,
- dumpManager
+ dumpManager,
) {
private val useExpandedOverlay: Boolean =
featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
- private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER)
+ private val isModernAlternateBouncerEnabled: Boolean =
+ featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)
private var showingUdfpsBouncer = false
private var udfpsRequested = false
private var qsExpansion = 0f
@@ -91,7 +91,6 @@
private var statusBarState = 0
private var transitionToFullShadeProgress = 0f
private var lastDozeAmount = 0f
- private var lastUdfpsBouncerShowTime: Long = -1
private var panelExpansionFraction = 0f
private var launchTransitionFadingAway = false
private var isLaunchingActivity = false
@@ -108,12 +107,6 @@
)
}
}
- /**
- * Hidden amount of input (pin/pattern/password) bouncer. This is used
- * [KeyguardBouncer.EXPANSION_VISIBLE] (0f) to [KeyguardBouncer.EXPANSION_HIDDEN] (1f). Only
- * used for the non-modernBouncer.
- */
- private var inputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN
private var inputBouncerExpansion = 0f // only used for modernBouncer
private val stateListener: StatusBarStateController.StateListener =
@@ -147,21 +140,6 @@
}
}
- private val mPrimaryBouncerExpansionCallback: PrimaryBouncerExpansionCallback =
- object : PrimaryBouncerExpansionCallback {
- override fun onExpansionChanged(expansion: Float) {
- inputBouncerHiddenAmount = expansion
- updateAlpha()
- updatePauseAuth()
- }
-
- override fun onVisibilityChanged(isVisible: Boolean) {
- updateBouncerHiddenAmount()
- updateAlpha()
- updatePauseAuth()
- }
- }
-
private val configurationListener: ConfigurationController.ConfigurationListener =
object : ConfigurationController.ConfigurationListener {
override fun onUiModeChanged() {
@@ -244,20 +222,8 @@
}
}
- private val mAlternateBouncer: AlternateBouncer =
- object : AlternateBouncer {
- override fun showAlternateBouncer(): Boolean {
- return showUdfpsBouncer(true)
- }
-
- override fun hideAlternateBouncer(): Boolean {
- return showUdfpsBouncer(false)
- }
-
- override fun isShowingAlternateBouncer(): Boolean {
- return showingUdfpsBouncer
- }
-
+ private val occludingAppBiometricUI: OccludingAppBiometricUI =
+ object : OccludingAppBiometricUI {
override fun requestUdfps(request: Boolean, color: Int) {
udfpsRequested = request
view.requestUdfps(request, color)
@@ -275,16 +241,17 @@
override fun onInit() {
super.onInit()
- keyguardViewManager.setAlternateBouncer(mAlternateBouncer)
+ keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
}
init {
- if (isModernBouncerEnabled) {
- view.repeatWhenAttached {
- // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
- // can make the view not visible; and we still want to listen for events
- // that may make the view visible again.
- repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) }
+ view.repeatWhenAttached {
+ // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
+ // can make the view not visible; and we still want to listen for events
+ // that may make the view visible again.
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ listenForBouncerExpansion(this)
+ if (isModernAlternateBouncerEnabled) listenForAlternateBouncerVisibility(this)
}
}
}
@@ -300,8 +267,18 @@
}
}
+ @VisibleForTesting
+ internal suspend fun listenForAlternateBouncerVisibility(scope: CoroutineScope): Job {
+ return scope.launch {
+ alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
+ showUdfpsBouncer(isVisible)
+ }
+ }
+ }
+
public override fun onViewAttached() {
super.onViewAttached()
+ alternateBouncerInteractor.setAlternateBouncerUIAvailable(true)
val dozeAmount = statusBarStateController.dozeAmount
lastDozeAmount = dozeAmount
stateListener.onDozeAmountChanged(dozeAmount, dozeAmount)
@@ -312,21 +289,14 @@
statusBarState = statusBarStateController.state
qsExpansion = keyguardViewManager.qsExpansion
keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback)
- if (!isModernBouncerEnabled) {
- val bouncer = keyguardViewManager.primaryBouncer
- bouncer?.expansion?.let {
- mPrimaryBouncerExpansionCallback.onExpansionChanged(it)
- bouncer.addBouncerExpansionCallback(mPrimaryBouncerExpansionCallback)
- }
- updateBouncerHiddenAmount()
- }
configurationController.addCallback(configurationListener)
shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
updateScaleFactor()
view.updatePadding()
updateAlpha()
updatePauseAuth()
- keyguardViewManager.setAlternateBouncer(mAlternateBouncer)
+ keyguardViewManager.setLegacyAlternateBouncer(legacyAlternateBouncer)
+ keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI)
lockScreenShadeTransitionController.udfpsKeyguardViewController = this
activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
view.mUseExpandedOverlay = useExpandedOverlay
@@ -334,10 +304,12 @@
override fun onViewDetached() {
super.onViewDetached()
+ alternateBouncerInteractor.setAlternateBouncerUIAvailable(false)
faceDetectRunning = false
keyguardStateController.removeCallback(keyguardStateControllerCallback)
statusBarStateController.removeCallback(stateListener)
- keyguardViewManager.removeAlternateAuthInterceptor(mAlternateBouncer)
+ keyguardViewManager.removeLegacyAlternateBouncer(legacyAlternateBouncer)
+ keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI)
keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
configurationController.removeCallback(configurationListener)
shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
@@ -346,17 +318,20 @@
}
activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
- if (!isModernBouncerEnabled) {
- keyguardViewManager.primaryBouncer?.removeBouncerExpansionCallback(
- mPrimaryBouncerExpansionCallback
- )
- }
}
override fun dump(pw: PrintWriter, args: Array<String>) {
super.dump(pw, args)
- pw.println("isModernBouncerEnabled=$isModernBouncerEnabled")
+ pw.println("isModernAlternateBouncerEnabled=$isModernAlternateBouncerEnabled")
pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer")
+ pw.println(
+ "altBouncerInteractor#isAlternateBouncerVisible=" +
+ "${alternateBouncerInteractor.isVisibleState()}"
+ )
+ pw.println(
+ "altBouncerInteractor#canShowAlternateBouncerForFingerprint=" +
+ "${alternateBouncerInteractor.canShowAlternateBouncerForFingerprint()}"
+ )
pw.println("faceDetectRunning=$faceDetectRunning")
pw.println("statusBarState=" + StatusBarState.toString(statusBarState))
pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress")
@@ -366,11 +341,7 @@
pw.println("udfpsRequestedByApp=$udfpsRequested")
pw.println("launchTransitionFadingAway=$launchTransitionFadingAway")
pw.println("lastDozeAmount=$lastDozeAmount")
- if (isModernBouncerEnabled) {
- pw.println("inputBouncerExpansion=$inputBouncerExpansion")
- } else {
- pw.println("inputBouncerHiddenAmount=$inputBouncerHiddenAmount")
- }
+ pw.println("inputBouncerExpansion=$inputBouncerExpansion")
view.dump(pw)
}
@@ -385,9 +356,6 @@
val udfpsAffordanceWasNotShowing = shouldPauseAuth()
showingUdfpsBouncer = show
if (showingUdfpsBouncer) {
- lastUdfpsBouncerShowTime = systemClock.uptimeMillis()
- }
- if (showingUdfpsBouncer) {
if (udfpsAffordanceWasNotShowing) {
view.animateInUdfpsBouncer(null)
}
@@ -400,7 +368,6 @@
} else {
keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
}
- updateBouncerHiddenAmount()
updateAlpha()
updatePauseAuth()
return true
@@ -441,49 +408,17 @@
}
fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean {
- return if (isModernBouncerEnabled) {
- inputBouncerExpansion >= bouncerExpansionThreshold
- } else {
- inputBouncerHiddenAmount < bouncerExpansionThreshold
- }
+ return inputBouncerExpansion >= bouncerExpansionThreshold
}
fun isInputBouncerFullyVisible(): Boolean {
- return if (isModernBouncerEnabled) {
- inputBouncerExpansion == 1f
- } else {
- keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateBouncer
- }
+ return inputBouncerExpansion == 1f
}
override fun listenForTouchesOutsideView(): Boolean {
return true
}
- override fun onTouchOutsideView() {
- maybeShowInputBouncer()
- }
-
- /**
- * If we were previously showing the udfps bouncer, hide it and instead show the regular
- * (pin/pattern/password) bouncer.
- *
- * Does nothing if we weren't previously showing the UDFPS bouncer.
- */
- private fun maybeShowInputBouncer() {
- if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) {
- keyguardViewManager.showPrimaryBouncer(true)
- }
- }
-
- /**
- * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the
- * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer.
- */
- private fun hasUdfpsBouncerShownWithMinTime(): Boolean {
- return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200
- }
-
/**
* Set the progress we're currently transitioning to the full shade. 0.0f means we're not
* transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down
@@ -529,11 +464,7 @@
}
private fun getInputBouncerHiddenAmt(): Float {
- return if (isModernBouncerEnabled) {
- 1f - inputBouncerExpansion
- } else {
- inputBouncerHiddenAmount
- }
+ return 1f - inputBouncerExpansion
}
/** Update the scale factor based on the device's resolution. */
@@ -541,18 +472,20 @@
udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) }
}
- private fun updateBouncerHiddenAmount() {
- if (isModernBouncerEnabled) {
- return
+ private val legacyAlternateBouncer: LegacyAlternateBouncer =
+ object : LegacyAlternateBouncer {
+ override fun showAlternateBouncer(): Boolean {
+ return showUdfpsBouncer(true)
+ }
+
+ override fun hideAlternateBouncer(): Boolean {
+ return showUdfpsBouncer(false)
+ }
+
+ override fun isShowingAlternateBouncer(): Boolean {
+ return showingUdfpsBouncer
+ }
}
- val altBouncerShowing = keyguardViewManager.isShowingAlternateBouncer
- if (altBouncerShowing || !keyguardViewManager.primaryBouncerIsOrWillBeShowing()) {
- inputBouncerHiddenAmount = 1f
- } else if (keyguardViewManager.isBouncerShowing) {
- // input bouncer is fully showing
- inputBouncerHiddenAmount = 0f
- }
- }
companion object {
const val TAG = "UdfpsKeyguardViewController"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
index 4a8877e..e61c614 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
@@ -111,10 +111,6 @@
}
}
- fun onTouchOutsideView() {
- animationViewController?.onTouchOutsideView()
- }
-
override fun onAttachedToWindow() {
super.onAttachedToWindow()
Log.v(TAG, "onAttachedToWindow")
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
index 8572242..682d38a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt
@@ -18,6 +18,7 @@
import android.graphics.Point
import android.graphics.Rect
+import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import kotlin.math.cos
import kotlin.math.pow
@@ -50,7 +51,8 @@
return result <= 1
}
- private fun calculateSensorPoints(sensorBounds: Rect): List<Point> {
+ @VisibleForTesting
+ fun calculateSensorPoints(sensorBounds: Rect): List<Point> {
val sensorX = sensorBounds.centerX()
val sensorY = sensorBounds.centerY()
val cornerOffset: Int = sensorBounds.width() / 4
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
index 62bedc6..48d4845 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt
@@ -26,28 +26,28 @@
* Value obtained from [MotionEvent.getPointerId], or [MotionEvent.INVALID_POINTER_ID] if the ID
* is not available.
*/
- val pointerId: Int,
+ val pointerId: Int = MotionEvent.INVALID_POINTER_ID,
/** [MotionEvent.getRawX] mapped to natural orientation and native resolution. */
- val x: Float,
+ val x: Float = 0f,
/** [MotionEvent.getRawY] mapped to natural orientation and native resolution. */
- val y: Float,
+ val y: Float = 0f,
/** [MotionEvent.getTouchMinor] mapped to natural orientation and native resolution. */
- val minor: Float,
+ val minor: Float = 0f,
/** [MotionEvent.getTouchMajor] mapped to natural orientation and native resolution. */
- val major: Float,
+ val major: Float = 0f,
/** [MotionEvent.getOrientation] mapped to natural orientation. */
- val orientation: Float,
+ val orientation: Float = 0f,
/** [MotionEvent.getEventTime]. */
- val time: Long,
+ val time: Long = 0,
/** [MotionEvent.getDownTime]. */
- val gestureStart: Long,
+ val gestureStart: Long = 0,
) {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
index 338bf66..39ea936 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
@@ -27,6 +27,8 @@
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
+private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270)
+
/**
* TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations.
*/
@@ -41,74 +43,75 @@
): TouchProcessorResult {
fun preprocess(): PreprocessedTouch {
- // TODO(b/253085297): Add multitouch support. pointerIndex can be > 0 for ACTION_MOVE.
- val pointerIndex = 0
- val touchData = event.normalize(pointerIndex, overlayParams)
- val isGoodOverlap =
- overlapDetector.isGoodOverlap(touchData, overlayParams.nativeSensorBounds)
- return PreprocessedTouch(touchData, previousPointerOnSensorId, isGoodOverlap)
+ val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) }
+ val pointersOnSensor =
+ touchData
+ .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) }
+ .map { it.pointerId }
+ return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor)
}
return when (event.actionMasked) {
- MotionEvent.ACTION_DOWN -> processActionDown(preprocess())
- MotionEvent.ACTION_MOVE -> processActionMove(preprocess())
- MotionEvent.ACTION_UP -> processActionUp(preprocess())
- MotionEvent.ACTION_CANCEL ->
- processActionCancel(event.normalize(pointerIndex = 0, overlayParams))
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_POINTER_DOWN,
+ MotionEvent.ACTION_MOVE,
+ MotionEvent.ACTION_HOVER_ENTER,
+ MotionEvent.ACTION_HOVER_MOVE -> processActionMove(preprocess())
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_POINTER_UP,
+ MotionEvent.ACTION_HOVER_EXIT ->
+ processActionUp(preprocess(), event.getPointerId(event.actionIndex))
+ MotionEvent.ACTION_CANCEL -> processActionCancel(NormalizedTouchData())
else ->
Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked))
}
}
}
+/**
+ * [data] contains a list of NormalizedTouchData for pointers in the motionEvent ordered by
+ * pointerIndex
+ *
+ * [previousPointerOnSensorId] the pointerId of the previous pointer on the sensor,
+ * [MotionEvent.INVALID_POINTER_ID] if none
+ *
+ * [pointersOnSensor] contains a list of ids of pointers on the sensor
+ */
private data class PreprocessedTouch(
- val data: NormalizedTouchData,
+ val data: List<NormalizedTouchData>,
val previousPointerOnSensorId: Int,
- val isGoodOverlap: Boolean,
+ val pointersOnSensor: List<Int>,
)
-private fun processActionDown(touch: PreprocessedTouch): TouchProcessorResult {
- return if (touch.isGoodOverlap) {
- ProcessedTouch(InteractionEvent.DOWN, pointerOnSensorId = touch.data.pointerId, touch.data)
- } else {
- val event =
- if (touch.data.pointerId == touch.previousPointerOnSensorId) {
- InteractionEvent.UP
- } else {
- InteractionEvent.UNCHANGED
- }
- ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
- }
-}
-
private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult {
val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID
- val interactionEvent =
- when {
- touch.isGoodOverlap && !hadPointerOnSensor -> InteractionEvent.DOWN
- !touch.isGoodOverlap && hadPointerOnSensor -> InteractionEvent.UP
- else -> InteractionEvent.UNCHANGED
- }
- val pointerOnSensorId =
- when (interactionEvent) {
- InteractionEvent.UNCHANGED -> touch.previousPointerOnSensorId
- InteractionEvent.DOWN -> touch.data.pointerId
- else -> INVALID_POINTER_ID
- }
- return ProcessedTouch(interactionEvent, pointerOnSensorId, touch.data)
+ val hasPointerOnSensor = touch.pointersOnSensor.isNotEmpty()
+ val pointerOnSensorId = touch.pointersOnSensor.firstOrNull() ?: INVALID_POINTER_ID
+
+ return if (!hadPointerOnSensor && hasPointerOnSensor) {
+ val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
+ ProcessedTouch(InteractionEvent.DOWN, data.pointerId, data)
+ } else if (hadPointerOnSensor && !hasPointerOnSensor) {
+ ProcessedTouch(InteractionEvent.UP, INVALID_POINTER_ID, NormalizedTouchData())
+ } else {
+ val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
+ ProcessedTouch(InteractionEvent.UNCHANGED, pointerOnSensorId, data)
+ }
}
-private fun processActionUp(touch: PreprocessedTouch): TouchProcessorResult {
- return if (touch.isGoodOverlap) {
- ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
+private fun processActionUp(touch: PreprocessedTouch, actionId: Int): TouchProcessorResult {
+ // Finger lifted and it was the only finger on the sensor
+ return if (touch.pointersOnSensor.size == 1 && touch.pointersOnSensor.contains(actionId)) {
+ ProcessedTouch(
+ InteractionEvent.UP,
+ pointerOnSensorId = INVALID_POINTER_ID,
+ NormalizedTouchData()
+ )
} else {
- val event =
- if (touch.previousPointerOnSensorId != INVALID_POINTER_ID) {
- InteractionEvent.UP
- } else {
- InteractionEvent.UNCHANGED
- }
- ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
+ // Pick new pointerOnSensor that's not the finger that was lifted
+ val pointerOnSensorId = touch.pointersOnSensor.find { it != actionId } ?: INVALID_POINTER_ID
+ val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
+ ProcessedTouch(InteractionEvent.UNCHANGED, data.pointerId, data)
}
}
@@ -129,19 +132,27 @@
val nativeY = naturalTouch.y / overlayParams.scaleFactor
val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor
val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor
+ var nativeOrientation: Float = getOrientation(pointerIndex)
+ if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) {
+ nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat()
+ }
return NormalizedTouchData(
pointerId = getPointerId(pointerIndex),
x = nativeX,
y = nativeY,
minor = nativeMinor,
major = nativeMajor,
- // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O.
- orientation = getOrientation(pointerIndex),
+ orientation = nativeOrientation,
time = eventTime,
gestureStart = downTime,
)
}
+private fun toRadVerticalFromRotated(rad: Double): Double {
+ val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI
+ return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI
+}
+
/**
* Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device
* is in the [Surface.ROTATION_0] orientation.
@@ -152,7 +163,7 @@
): PointF {
val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex))
val rot = overlayParams.rotation
- if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+ if (SUPPORTED_ROTATIONS.contains(rot)) {
RotationUtils.rotatePointF(
touchPoint,
RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index 58d40d3..4227a7a 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
@@ -25,13 +25,13 @@
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.RemoteException
-import android.os.UserHandle
import android.util.Log
import android.view.WindowManager
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.system.ActivityManagerKt.isInForeground
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -55,6 +55,7 @@
private val cameraIntents: CameraIntentsWrapper,
private val contentResolver: ContentResolver,
@Main private val uiExecutor: Executor,
+ private val userTracker: UserTracker
) {
/**
* Whether the camera application can be launched for the camera launch gesture.
@@ -111,7 +112,7 @@
Intent.FLAG_ACTIVITY_NEW_TASK,
null,
activityOptions.toBundle(),
- UserHandle.CURRENT.identifier,
+ userTracker.userId,
)
} catch (e: RemoteException) {
Log.w(
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
index 867faf9..cc43e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt
@@ -20,15 +20,13 @@
import android.content.Intent
import android.provider.MediaStore
import android.text.TextUtils
-
import com.android.systemui.R
class CameraIntents {
companion object {
- val DEFAULT_SECURE_CAMERA_INTENT_ACTION =
- MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE
- val DEFAULT_INSECURE_CAMERA_INTENT_ACTION =
- MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
+ val DEFAULT_SECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE
+ val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA
+ private val VIDEO_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_VIDEO_CAMERA
const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source"
@JvmStatic
@@ -44,18 +42,14 @@
@JvmStatic
fun getInsecureCameraIntent(context: Context): Intent {
val intent = Intent(DEFAULT_INSECURE_CAMERA_INTENT_ACTION)
- getOverrideCameraPackage(context)?.let {
- intent.setPackage(it)
- }
+ getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
return intent
}
@JvmStatic
fun getSecureCameraIntent(context: Context): Intent {
val intent = Intent(DEFAULT_SECURE_CAMERA_INTENT_ACTION)
- getOverrideCameraPackage(context)?.let {
- intent.setPackage(it)
- }
+ getOverrideCameraPackage(context)?.let { intent.setPackage(it) }
return intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
}
@@ -68,5 +62,11 @@
fun isInsecureCameraIntent(intent: Intent?): Boolean {
return intent?.getAction()?.equals(DEFAULT_INSECURE_CAMERA_INTENT_ACTION) ?: false
}
+
+ /** Returns an [Intent] that can be used to start the camera in video mode. */
+ @JvmStatic
+ fun getVideoCameraIntent(): Intent {
+ return Intent(VIDEO_CAMERA_INTENT_ACTION)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
index cf02f8f..a434617 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntentsWrapper.kt
@@ -21,7 +21,9 @@
import javax.inject.Inject
/** Injectable wrapper around [CameraIntents]. */
-class CameraIntentsWrapper @Inject constructor(
+class CameraIntentsWrapper
+@Inject
+constructor(
private val context: Context,
) {
@@ -40,4 +42,9 @@
fun getInsecureCameraIntent(): Intent {
return CameraIntents.getInsecureCameraIntent(context)
}
+
+ /** Returns an [Intent] that can be used to start the camera in video mode. */
+ fun getVideoCameraIntent(): Intent {
+ return CameraIntents.getVideoCameraIntent()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index 1454210..fb0c0a6 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -20,7 +20,7 @@
import android.content.res.Configuration
import android.graphics.PixelFormat
import android.os.SystemProperties
-import android.util.DisplayMetrics
+import android.view.Surface
import android.view.View
import android.view.WindowManager
import com.android.internal.annotations.VisibleForTesting
@@ -36,7 +36,6 @@
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.leak.RotationUtils
import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter
import javax.inject.Inject
@@ -172,30 +171,28 @@
}
private fun layoutRipple() {
- val displayMetrics = DisplayMetrics()
- context.display.getRealMetrics(displayMetrics)
- val width = displayMetrics.widthPixels
- val height = displayMetrics.heightPixels
+ val bounds = windowManager.currentWindowMetrics.bounds
+ val width = bounds.width()
+ val height = bounds.height()
val maxDiameter = Integer.max(width, height) * 2f
rippleView.setMaxSize(maxDiameter, maxDiameter)
- when (RotationUtils.getExactRotation(context)) {
- RotationUtils.ROTATION_LANDSCAPE -> {
+ when (context.display.rotation) {
+ Surface.ROTATION_0 -> {
+ rippleView.setCenter(
+ width * normalizedPortPosX, height * normalizedPortPosY)
+ }
+ Surface.ROTATION_90 -> {
rippleView.setCenter(
width * normalizedPortPosY, height * (1 - normalizedPortPosX))
}
- RotationUtils.ROTATION_UPSIDE_DOWN -> {
+ Surface.ROTATION_180 -> {
rippleView.setCenter(
width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
}
- RotationUtils.ROTATION_SEASCAPE -> {
+ Surface.ROTATION_270 -> {
rippleView.setCenter(
width * (1 - normalizedPortPosY), height * normalizedPortPosX)
}
- else -> {
- // ROTATION_NONE
- rippleView.setCenter(
- width * normalizedPortPosX, height * normalizedPortPosY)
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index e8e1f2e..e9ac840 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -176,7 +176,8 @@
private @Classifier.InteractionType int mPriorInteractionType = Classifier.GENERIC;
@Inject
- public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider,
+ public BrightLineFalsingManager(
+ FalsingDataProvider falsingDataProvider,
MetricsLogger metricsLogger,
@Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers,
SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier,
@@ -399,7 +400,9 @@
|| mDataProvider.isJustUnlockedWithFace()
|| mDataProvider.isDocked()
|| mAccessibilityManager.isTouchExplorationEnabled()
- || mDataProvider.isA11yAction();
+ || mDataProvider.isA11yAction()
+ || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED)
+ && !mDataProvider.isFolded());
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index 09ebeea..5f347c1 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -16,6 +16,7 @@
package com.android.systemui.classifier;
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
@@ -42,6 +43,7 @@
private final int mWidthPixels;
private final int mHeightPixels;
private BatteryController mBatteryController;
+ private final FoldStateListener mFoldStateListener;
private final DockManager mDockManager;
private final float mXdpi;
private final float mYdpi;
@@ -65,12 +67,14 @@
public FalsingDataProvider(
DisplayMetrics displayMetrics,
BatteryController batteryController,
+ FoldStateListener foldStateListener,
DockManager dockManager) {
mXdpi = displayMetrics.xdpi;
mYdpi = displayMetrics.ydpi;
mWidthPixels = displayMetrics.widthPixels;
mHeightPixels = displayMetrics.heightPixels;
mBatteryController = batteryController;
+ mFoldStateListener = foldStateListener;
mDockManager = dockManager;
FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi());
@@ -376,6 +380,10 @@
return mBatteryController.isWirelessCharging() || mDockManager.isDocked();
}
+ public boolean isFolded() {
+ return Boolean.TRUE.equals(mFoldStateListener.getFolded());
+ }
+
/** Implement to be alerted abotu the beginning and ending of falsing tracking. */
public interface SessionListener {
/** Called when the lock screen is shown and falsing-tracking begins. */
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 805a20a..1c26841 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -18,7 +18,6 @@
import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
@@ -29,7 +28,6 @@
import android.content.ClipboardManager;
import android.content.Context;
import android.os.SystemProperties;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.util.Log;
@@ -37,9 +35,6 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.util.DeviceConfigProxy;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -59,42 +54,28 @@
"com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY";
private final Context mContext;
- private final DeviceConfigProxy mDeviceConfig;
private final Provider<ClipboardOverlayController> mOverlayProvider;
- private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
private final ClipboardToast mClipboardToast;
private final ClipboardManager mClipboardManager;
private final UiEventLogger mUiEventLogger;
- private final FeatureFlags mFeatureFlags;
- private boolean mUsingNewOverlay;
private ClipboardOverlay mClipboardOverlay;
@Inject
- public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
+ public ClipboardListener(Context context,
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
- ClipboardOverlayControllerLegacyFactory overlayFactory,
ClipboardToast clipboardToast,
ClipboardManager clipboardManager,
- UiEventLogger uiEventLogger,
- FeatureFlags featureFlags) {
+ UiEventLogger uiEventLogger) {
mContext = context;
- mDeviceConfig = deviceConfigProxy;
mOverlayProvider = clipboardOverlayControllerProvider;
- mOverlayFactory = overlayFactory;
mClipboardToast = clipboardToast;
mClipboardManager = clipboardManager;
mUiEventLogger = uiEventLogger;
- mFeatureFlags = featureFlags;
-
- mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
}
@Override
public void start() {
- if (mDeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, true)) {
- mClipboardManager.addPrimaryClipChangedListener(this);
- }
+ mClipboardManager.addPrimaryClipChangedListener(this);
}
@Override
@@ -120,14 +101,8 @@
return;
}
- boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
- if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
- mUsingNewOverlay = enabled;
- if (enabled) {
- mClipboardOverlay = mOverlayProvider.get();
- } else {
- mClipboardOverlay = mOverlayFactory.create(mContext);
- }
+ if (mClipboardOverlay == null) {
+ mClipboardOverlay = mOverlayProvider.get();
mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
} else {
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index f97d6af..8c8ee8a 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -17,7 +17,6 @@
package com.android.systemui.clipboardoverlay;
import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
@@ -72,6 +71,7 @@
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.TimeoutHandler;
+import com.android.systemui.settings.DisplayTracker;
import java.io.IOException;
import java.util.Optional;
@@ -96,6 +96,7 @@
private final ClipboardLogger mClipboardLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
private final DisplayManager mDisplayManager;
+ private final DisplayTracker mDisplayTracker;
private final ClipboardOverlayWindow mWindow;
private final TimeoutHandler mTimeoutHandler;
private final ClipboardOverlayUtils mClipboardUtils;
@@ -186,9 +187,11 @@
FeatureFlags featureFlags,
ClipboardOverlayUtils clipboardUtils,
@Background Executor bgExecutor,
- UiEventLogger uiEventLogger) {
+ UiEventLogger uiEventLogger,
+ DisplayTracker displayTracker) {
mBroadcastDispatcher = broadcastDispatcher;
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+ mDisplayTracker = displayTracker;
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
@@ -514,7 +517,7 @@
}
private Display getDefaultDisplay() {
- return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
+ return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
}
static class ClipboardLogger {
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
deleted file mode 100644
index 3a040829..0000000
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java
+++ /dev/null
@@ -1,963 +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.clipboardoverlay;
-
-import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
-
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
-import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
-
-import static java.util.Objects.requireNonNull;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.MainThread;
-import android.app.ICompatCameraControlCallback;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.Icon;
-import android.hardware.display.DisplayManager;
-import android.hardware.input.InputManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Looper;
-import android.provider.DeviceConfig;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.MathUtils;
-import android.util.Size;
-import android.util.TypedValue;
-import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.Gravity;
-import android.view.InputEvent;
-import android.view.InputEventReceiver;
-import android.view.InputMonitor;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.LinearInterpolator;
-import android.view.animation.PathInterpolator;
-import android.view.textclassifier.TextClassification;
-import android.view.textclassifier.TextClassificationManager;
-import android.view.textclassifier.TextClassifier;
-import android.view.textclassifier.TextLinks;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.PhoneWindow;
-import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.screenshot.DraggableConstraintLayout;
-import com.android.systemui.screenshot.FloatingWindowUtil;
-import com.android.systemui.screenshot.OverlayActionChip;
-import com.android.systemui.screenshot.TimeoutHandler;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * Controls state and UI for the overlay that appears when something is added to the clipboard
- */
-public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay {
- private static final String TAG = "ClipboardOverlayCtrlr";
- private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY";
-
- /** Constants for screenshot/copy deconflicting */
- public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT";
- public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF";
- public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
-
- private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard";
-
- private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
- private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
- private static final int FONT_SEARCH_STEP_PX = 4;
-
- private final Context mContext;
- private final ClipboardLogger mClipboardLogger;
- private final BroadcastDispatcher mBroadcastDispatcher;
- private final DisplayManager mDisplayManager;
- private final DisplayMetrics mDisplayMetrics;
- private final WindowManager mWindowManager;
- private final WindowManager.LayoutParams mWindowLayoutParams;
- private final PhoneWindow mWindow;
- private final TimeoutHandler mTimeoutHandler;
- private final AccessibilityManager mAccessibilityManager;
- private final TextClassifier mTextClassifier;
-
- private final DraggableConstraintLayout mView;
- private final View mClipboardPreview;
- private final ImageView mImagePreview;
- private final TextView mTextPreview;
- private final TextView mHiddenPreview;
- private final View mPreviewBorder;
- private final OverlayActionChip mEditChip;
- private final OverlayActionChip mShareChip;
- private final OverlayActionChip mRemoteCopyChip;
- private final View mActionContainerBackground;
- private final View mDismissButton;
- private final LinearLayout mActionContainer;
- private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
-
- private Runnable mOnSessionCompleteListener;
-
- private InputMonitor mInputMonitor;
- private InputEventReceiver mInputEventReceiver;
-
- private BroadcastReceiver mCloseDialogsReceiver;
- private BroadcastReceiver mScreenshotReceiver;
-
- private boolean mBlockAttach = false;
- private Animator mExitAnimator;
- private Animator mEnterAnimator;
- private final int mOrientation;
- private boolean mKeyboardVisible;
-
-
- public ClipboardOverlayControllerLegacy(Context context,
- BroadcastDispatcher broadcastDispatcher,
- BroadcastSender broadcastSender,
- TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
- mBroadcastDispatcher = broadcastDispatcher;
- mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
- final Context displayContext = context.createDisplayContext(getDefaultDisplay());
- mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
-
- mClipboardLogger = new ClipboardLogger(uiEventLogger);
-
- mAccessibilityManager = AccessibilityManager.getInstance(mContext);
- mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
- .getTextClassifier();
-
- mWindowManager = mContext.getSystemService(WindowManager.class);
-
- mDisplayMetrics = new DisplayMetrics();
- mContext.getDisplay().getRealMetrics(mDisplayMetrics);
-
- mTimeoutHandler = timeoutHandler;
- mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
-
- // Setup the window that we are going to use
- mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
- mWindowLayoutParams.setTitle("ClipboardOverlay");
-
- mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
- mWindow.setWindowManager(mWindowManager, null, null);
-
- setWindowFocusable(false);
-
- mView = (DraggableConstraintLayout)
- LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null);
- mActionContainerBackground =
- requireNonNull(mView.findViewById(R.id.actions_container_background));
- mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
- mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview));
- mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
- mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
- mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview));
- mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border));
- mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
- mShareChip = requireNonNull(mView.findViewById(R.id.share_chip));
- mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip));
- mEditChip.setAlpha(1);
- mShareChip.setAlpha(1);
- mRemoteCopyChip.setAlpha(1);
- mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
-
- mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
- mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
- @Override
- public void onInteraction() {
- mTimeoutHandler.resetTimeout();
- }
-
- @Override
- public void onSwipeDismissInitiated(Animator animator) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
- mExitAnimator = animator;
- }
-
- @Override
- public void onDismissComplete() {
- hideImmediate();
- }
- });
-
- mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
- int availableHeight = mTextPreview.getHeight()
- - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
- mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight());
- return true;
- });
-
- mDismissButton.setOnClickListener(view -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
- animateOut();
- });
-
- mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
- mRemoteCopyChip.setIcon(
- Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
- mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
- mOrientation = mContext.getResources().getConfiguration().orientation;
-
- attachWindow();
- withWindowAttached(() -> {
- mWindow.setContentView(mView);
- WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
- mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
- updateInsets(insets);
- mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- WindowInsets insets =
- mWindowManager.getCurrentWindowMetrics().getWindowInsets();
- boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
- if (keyboardVisible != mKeyboardVisible) {
- mKeyboardVisible = keyboardVisible;
- updateInsets(insets);
- }
- }
- });
- mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
- new ViewRootImpl.ActivityConfigCallback() {
- @Override
- public void onConfigurationChanged(Configuration overrideConfig,
- int newDisplayId) {
- if (mContext.getResources().getConfiguration().orientation
- != mOrientation) {
- mClipboardLogger.logSessionComplete(
- CLIPBOARD_OVERLAY_DISMISSED_OTHER);
- hideImmediate();
- }
- }
-
- @Override
- public void requestCompatCameraControl(
- boolean showControl, boolean transformationApplied,
- ICompatCameraControlCallback callback) {
- Log.w(TAG, "unexpected requestCompatCameraControl call");
- }
- });
- });
-
- mTimeoutHandler.setOnTimeoutRunnable(() -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
- animateOut();
- });
-
- mCloseDialogsReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
- animateOut();
- }
- }
- };
-
- mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
- new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
- mScreenshotReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (SCREENSHOT_ACTION.equals(intent.getAction())) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
- animateOut();
- }
- }
- };
-
- mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
- new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
- SELF_PERMISSION);
- monitorOutsideTouches();
-
- Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
- // Set package name so the system knows it's safe
- copyIntent.setPackage(mContext.getPackageName());
- broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
- }
-
- @Override // ClipboardListener.ClipboardOverlay
- public void setClipData(ClipData clipData, String clipSource) {
- if (mExitAnimator != null && mExitAnimator.isRunning()) {
- mExitAnimator.cancel();
- }
- reset();
- String accessibilityAnnouncement;
-
- boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
- && clipData.getDescription().getExtras()
- .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
- if (clipData == null || clipData.getItemCount() == 0) {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
- } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
- ClipData.Item item = clipData.getItemAt(0);
- if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
- if (item.getTextLinks() != null) {
- AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource));
- }
- }
- if (isSensitive) {
- showEditableText(
- mContext.getResources().getString(R.string.clipboard_asterisks), true);
- } else {
- showEditableText(item.getText(), false);
- }
- showShareChip(clipData);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
- } else if (clipData.getItemAt(0).getUri() != null) {
- if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
- showShareChip(clipData);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
- } else {
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
- }
- } else {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
- }
- Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
- // Only show remote copy if it's available.
- PackageManager packageManager = mContext.getPackageManager();
- if (packageManager.resolveActivity(
- remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
- mRemoteCopyChip.setContentDescription(
- mContext.getString(R.string.clipboard_send_nearby_description));
- mRemoteCopyChip.setVisibility(View.VISIBLE);
- mRemoteCopyChip.setOnClickListener((v) -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
- mContext.startActivity(remoteCopyIntent);
- animateOut();
- });
- mActionContainerBackground.setVisibility(View.VISIBLE);
- } else {
- mRemoteCopyChip.setVisibility(View.GONE);
- }
- withWindowAttached(() -> {
- if (mEnterAnimator == null || !mEnterAnimator.isRunning()) {
- mView.post(this::animateIn);
- }
- mView.announceForAccessibility(accessibilityAnnouncement);
- });
- mTimeoutHandler.resetTimeout();
- }
-
- @Override // ClipboardListener.ClipboardOverlay
- public void setOnSessionCompleteListener(Runnable runnable) {
- mOnSessionCompleteListener = runnable;
- }
-
- private void classifyText(ClipData.Item item, String source) {
- ArrayList<RemoteAction> actions = new ArrayList<>();
- for (TextLinks.TextLink link : item.getTextLinks().getLinks()) {
- TextClassification classification = mTextClassifier.classifyText(
- item.getText(), link.getStart(), link.getEnd(), null);
- actions.addAll(classification.getActions());
- }
- mView.post(() -> {
- resetActionChips();
- if (actions.size() > 0) {
- mActionContainerBackground.setVisibility(View.VISIBLE);
- for (RemoteAction action : actions) {
- Intent targetIntent = action.getActionIntent().getIntent();
- ComponentName component = targetIntent.getComponent();
- if (component != null && !TextUtils.equals(source,
- component.getPackageName())) {
- OverlayActionChip chip = constructActionChip(action);
- mActionContainer.addView(chip);
- mActionChips.add(chip);
- break; // only show at most one action chip
- }
- }
- }
- });
- }
-
- private void showShareChip(ClipData clip) {
- mShareChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mShareChip.setOnClickListener((v) -> shareContent(clip));
- }
-
- private OverlayActionChip constructActionChip(RemoteAction action) {
- OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
- R.layout.overlay_action_chip, mActionContainer, false);
- chip.setText(action.getTitle());
- chip.setContentDescription(action.getTitle());
- chip.setIcon(action.getIcon(), false);
- chip.setPendingIntent(action.getActionIntent(), () -> {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
- animateOut();
- });
- chip.setAlpha(1);
- return chip;
- }
-
- private void monitorOutsideTouches() {
- InputManager inputManager = mContext.getSystemService(InputManager.class);
- mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
- mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
- Looper.getMainLooper()) {
- @Override
- public void onInputEvent(InputEvent event) {
- if (event instanceof MotionEvent) {
- MotionEvent motionEvent = (MotionEvent) event;
- if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
- Region touchRegion = new Region();
-
- final Rect tmpRect = new Rect();
- mPreviewBorder.getBoundsOnScreen(tmpRect);
- tmpRect.inset(
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
- -SWIPE_PADDING_DP));
- touchRegion.op(tmpRect, Region.Op.UNION);
- mActionContainerBackground.getBoundsOnScreen(tmpRect);
- tmpRect.inset(
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
- (int) FloatingWindowUtil.dpToPx(mDisplayMetrics,
- -SWIPE_PADDING_DP));
- touchRegion.op(tmpRect, Region.Op.UNION);
- mDismissButton.getBoundsOnScreen(tmpRect);
- touchRegion.op(tmpRect, Region.Op.UNION);
- if (!touchRegion.contains(
- (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
- animateOut();
- }
- }
- }
- finishInputEvent(event, true /* handled */);
- }
- };
- }
-
- private void editImage(Uri uri) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
- mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
- animateOut();
- }
-
- private void editText() {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
- mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
- animateOut();
- }
-
- private void shareContent(ClipData clip) {
- mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
- mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
- animateOut();
- }
-
- private void showSinglePreview(View v) {
- mTextPreview.setVisibility(View.GONE);
- mImagePreview.setVisibility(View.GONE);
- mHiddenPreview.setVisibility(View.GONE);
- v.setVisibility(View.VISIBLE);
- }
-
- private void showTextPreview(CharSequence text, TextView textView) {
- showSinglePreview(textView);
- final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length()));
- textView.setText(truncatedText);
- updateTextSize(truncatedText, textView);
-
- textView.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- if (right - left != oldRight - oldLeft) {
- updateTextSize(truncatedText, textView);
- }
- });
- mEditChip.setVisibility(View.GONE);
- }
-
- private void updateTextSize(CharSequence text, TextView textView) {
- Paint paint = new Paint(textView.getPaint());
- Resources res = textView.getResources();
- float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
- float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
- if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
- // If the text is a single word and would fit within the TextView at the min font size,
- // find the biggest font size that will fit.
- float fontSizePx = minFontSize;
- while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
- && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
- fontSizePx += FONT_SEARCH_STEP_PX;
- }
- // Need to turn off autosizing, otherwise setTextSize is a no-op.
- textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
- // It's possible to hit the max font size and not fill the width, so centering
- // horizontally looks better in this case.
- textView.setGravity(Gravity.CENTER);
- textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
- } else {
- // Otherwise just stick with autosize.
- textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
- (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
- textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
- }
- }
-
- private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
- float fontSizePx) {
- paint.setTextSize(fontSizePx);
- float size = paint.measureText(text.toString());
- float availableWidth = textView.getWidth() - textView.getPaddingLeft()
- - textView.getPaddingRight();
- return size < availableWidth;
- }
-
- private static boolean isOneWord(CharSequence text) {
- return text.toString().split("\\s+", 2).length == 1;
- }
-
- private void showEditableText(CharSequence text, boolean hidden) {
- TextView textView = hidden ? mHiddenPreview : mTextPreview;
- showTextPreview(text, textView);
- View.OnClickListener listener = v -> editText();
- setAccessibilityActionToEdit(textView);
- if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
- mEditChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mEditChip.setContentDescription(
- mContext.getString(R.string.clipboard_edit_text_description));
- mEditChip.setOnClickListener(listener);
- }
- textView.setOnClickListener(listener);
- }
-
- private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
- View.OnClickListener listener = v -> editImage(uri);
- ContentResolver resolver = mContext.getContentResolver();
- String mimeType = resolver.getType(uri);
- boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
- if (isSensitive) {
- mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
- showSinglePreview(mHiddenPreview);
- if (isEditableImage) {
- mHiddenPreview.setOnClickListener(listener);
- setAccessibilityActionToEdit(mHiddenPreview);
- }
- } else if (isEditableImage) { // if the MIMEtype is image, try to load
- try {
- int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
- // The width of the view is capped, height maintains aspect ratio, so allow it to be
- // taller if needed.
- Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
- showSinglePreview(mImagePreview);
- mImagePreview.setImageBitmap(thumbnail);
- mImagePreview.setOnClickListener(listener);
- setAccessibilityActionToEdit(mImagePreview);
- } catch (IOException e) {
- Log.e(TAG, "Thumbnail loading failed", e);
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- isEditableImage = false;
- }
- } else {
- showTextPreview(
- mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
- mTextPreview);
- }
- if (isEditableImage && DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
- mEditChip.setVisibility(View.VISIBLE);
- mActionContainerBackground.setVisibility(View.VISIBLE);
- mEditChip.setOnClickListener(listener);
- mEditChip.setContentDescription(
- mContext.getString(R.string.clipboard_edit_image_description));
- }
- return isEditableImage;
- }
-
- private void setAccessibilityActionToEdit(View view) {
- ViewCompat.replaceAccessibilityAction(view,
- AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
- mContext.getString(R.string.clipboard_edit), null);
- }
-
- private void animateIn() {
- if (mAccessibilityManager.isEnabled()) {
- mDismissButton.setVisibility(View.VISIBLE);
- }
- mEnterAnimator = getEnterAnimation();
- mEnterAnimator.start();
- }
-
- private void animateOut() {
- if (mExitAnimator != null && mExitAnimator.isRunning()) {
- return;
- }
- Animator anim = getExitAnimation();
- anim.addListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationCancel(Animator animation) {
- super.onAnimationCancel(animation);
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- if (!mCancelled) {
- hideImmediate();
- }
- }
- });
- mExitAnimator = anim;
- anim.start();
- }
-
- private Animator getEnterAnimation() {
- TimeInterpolator linearInterpolator = new LinearInterpolator();
- TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
- AnimatorSet enterAnim = new AnimatorSet();
-
- ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
- rootAnim.setInterpolator(linearInterpolator);
- rootAnim.setDuration(66);
- rootAnim.addUpdateListener(animation -> {
- mView.setAlpha(animation.getAnimatedFraction());
- });
-
- ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
- scaleAnim.setInterpolator(scaleInterpolator);
- scaleAnim.setDuration(333);
- scaleAnim.addUpdateListener(animation -> {
- float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
- mClipboardPreview.setScaleX(previewScale);
- mClipboardPreview.setScaleY(previewScale);
- mPreviewBorder.setScaleX(previewScale);
- mPreviewBorder.setScaleY(previewScale);
-
- float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
- mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
- mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
- float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
- float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
- mActionContainer.setScaleX(actionsScaleX);
- mActionContainer.setScaleY(actionsScaleY);
- mActionContainerBackground.setScaleX(actionsScaleX);
- mActionContainerBackground.setScaleY(actionsScaleY);
- });
-
- ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
- alphaAnim.setInterpolator(linearInterpolator);
- alphaAnim.setDuration(283);
- alphaAnim.addUpdateListener(animation -> {
- float alpha = animation.getAnimatedFraction();
- mClipboardPreview.setAlpha(alpha);
- mPreviewBorder.setAlpha(alpha);
- mDismissButton.setAlpha(alpha);
- mActionContainer.setAlpha(alpha);
- });
-
- mActionContainer.setAlpha(0);
- mPreviewBorder.setAlpha(0);
- mClipboardPreview.setAlpha(0);
- enterAnim.play(rootAnim).with(scaleAnim);
- enterAnim.play(alphaAnim).after(50).after(rootAnim);
-
- enterAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- mView.setAlpha(1);
- mTimeoutHandler.resetTimeout();
- }
- });
- return enterAnim;
- }
-
- private Animator getExitAnimation() {
- TimeInterpolator linearInterpolator = new LinearInterpolator();
- TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
- AnimatorSet exitAnim = new AnimatorSet();
-
- ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
- rootAnim.setInterpolator(linearInterpolator);
- rootAnim.setDuration(100);
- rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction()));
-
- ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
- scaleAnim.setInterpolator(scaleInterpolator);
- scaleAnim.setDuration(250);
- scaleAnim.addUpdateListener(animation -> {
- float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
- mClipboardPreview.setScaleX(previewScale);
- mClipboardPreview.setScaleY(previewScale);
- mPreviewBorder.setScaleX(previewScale);
- mPreviewBorder.setScaleY(previewScale);
-
- float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
- mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
- mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
- float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
- float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
- mActionContainer.setScaleX(actionScaleX);
- mActionContainer.setScaleY(actionScaleY);
- mActionContainerBackground.setScaleX(actionScaleX);
- mActionContainerBackground.setScaleY(actionScaleY);
- });
-
- ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
- alphaAnim.setInterpolator(linearInterpolator);
- alphaAnim.setDuration(166);
- alphaAnim.addUpdateListener(animation -> {
- float alpha = 1 - animation.getAnimatedFraction();
- mClipboardPreview.setAlpha(alpha);
- mPreviewBorder.setAlpha(alpha);
- mDismissButton.setAlpha(alpha);
- mActionContainer.setAlpha(alpha);
- });
-
- exitAnim.play(alphaAnim).with(scaleAnim);
- exitAnim.play(rootAnim).after(150).after(alphaAnim);
- return exitAnim;
- }
-
- private void hideImmediate() {
- // Note this may be called multiple times if multiple dismissal events happen at the same
- // time.
- mTimeoutHandler.cancelTimeout();
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.removeViewImmediate(decorView);
- }
- if (mCloseDialogsReceiver != null) {
- mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
- mCloseDialogsReceiver = null;
- }
- if (mScreenshotReceiver != null) {
- mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver);
- mScreenshotReceiver = null;
- }
- if (mInputEventReceiver != null) {
- mInputEventReceiver.dispose();
- mInputEventReceiver = null;
- }
- if (mInputMonitor != null) {
- mInputMonitor.dispose();
- mInputMonitor = null;
- }
- if (mOnSessionCompleteListener != null) {
- mOnSessionCompleteListener.run();
- }
- }
-
- private void resetActionChips() {
- for (OverlayActionChip chip : mActionChips) {
- mActionContainer.removeView(chip);
- }
- mActionChips.clear();
- }
-
- private void reset() {
- mView.setTranslationX(0);
- mView.setAlpha(0);
- mActionContainerBackground.setVisibility(View.GONE);
- mShareChip.setVisibility(View.GONE);
- mEditChip.setVisibility(View.GONE);
- mRemoteCopyChip.setVisibility(View.GONE);
- resetActionChips();
- mTimeoutHandler.cancelTimeout();
- mClipboardLogger.reset();
- }
-
- @MainThread
- private void attachWindow() {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow() || mBlockAttach) {
- return;
- }
- mBlockAttach = true;
- mWindowManager.addView(decorView, mWindowLayoutParams);
- decorView.requestApplyInsets();
- mView.requestApplyInsets();
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mBlockAttach = false;
- }
-
- @Override
- public void onWindowDetached() {
- }
- }
- );
- }
-
- private void withWindowAttached(Runnable action) {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow()) {
- action.run();
- } else {
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mBlockAttach = false;
- decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
- action.run();
- }
-
- @Override
- public void onWindowDetached() {
- }
- });
- }
- }
-
- private void updateInsets(WindowInsets insets) {
- int orientation = mContext.getResources().getConfiguration().orientation;
- FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams();
- if (p == null) {
- return;
- }
- DisplayCutout cutout = insets.getDisplayCutout();
- Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
- Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
- if (cutout == null) {
- p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
- } else {
- Insets waterfall = cutout.getWaterfallInsets();
- if (orientation == ORIENTATION_PORTRAIT) {
- p.setMargins(
- waterfall.left,
- Math.max(cutout.getSafeInsetTop(), waterfall.top),
- waterfall.right,
- Math.max(imeInsets.bottom,
- Math.max(cutout.getSafeInsetBottom(),
- Math.max(navBarInsets.bottom, waterfall.bottom))));
- } else {
- p.setMargins(
- waterfall.left,
- waterfall.top,
- waterfall.right,
- Math.max(imeInsets.bottom,
- Math.max(navBarInsets.bottom, waterfall.bottom)));
- }
- }
- mView.setLayoutParams(p);
- mView.requestLayout();
- }
-
- private Display getDefaultDisplay() {
- return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
- }
-
- /**
- * Updates the window focusability. If the window is already showing, then it updates the
- * window immediately, otherwise the layout params will be applied when the window is next
- * shown.
- */
- private void setWindowFocusable(boolean focusable) {
- int flags = mWindowLayoutParams.flags;
- if (focusable) {
- mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- } else {
- mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- }
- if (mWindowLayoutParams.flags == flags) {
- return;
- }
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
- }
- }
-
- static class ClipboardLogger {
- private final UiEventLogger mUiEventLogger;
- private boolean mGuarded = false;
-
- ClipboardLogger(UiEventLogger uiEventLogger) {
- mUiEventLogger = uiEventLogger;
- }
-
- void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) {
- if (!mGuarded) {
- mGuarded = true;
- mUiEventLogger.log(event);
- }
- }
-
- void reset() {
- mGuarded = false;
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
deleted file mode 100644
index 0d989a7..0000000
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.clipboardoverlay;
-
-import android.content.Context;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.screenshot.TimeoutHandler;
-
-import javax.inject.Inject;
-
-/**
- * A factory that churns out ClipboardOverlayControllerLegacys on demand.
- */
-@SysUISingleton
-public class ClipboardOverlayControllerLegacyFactory {
-
- private final UiEventLogger mUiEventLogger;
- private final BroadcastDispatcher mBroadcastDispatcher;
- private final BroadcastSender mBroadcastSender;
-
- @Inject
- public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher,
- BroadcastSender broadcastSender, UiEventLogger uiEventLogger) {
- this.mBroadcastDispatcher = broadcastDispatcher;
- this.mBroadcastSender = broadcastSender;
- this.mUiEventLogger = uiEventLogger;
- }
-
- /**
- * One new ClipboardOverlayControllerLegacy, coming right up!
- */
- public ClipboardOverlayControllerLegacy create(Context context) {
- return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender,
- new TimeoutHandler(context), mUiEventLogger);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
index 2244813..09b2e44 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -16,7 +16,6 @@
package com.android.systemui.clipboardoverlay.dagger;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -28,6 +27,7 @@
import com.android.systemui.R;
import com.android.systemui.clipboardoverlay.ClipboardOverlayView;
+import com.android.systemui.settings.DisplayTracker;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -46,8 +46,9 @@
*/
@Provides
@OverlayWindowContext
- static Context provideWindowContext(DisplayManager displayManager, Context context) {
- Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
+ static Context provideWindowContext(DisplayManager displayManager,
+ DisplayTracker displayTracker, Context context) {
+ Display display = displayManager.getDisplay(displayTracker.getDefaultDisplayId());
return context.createWindowContext(display, TYPE_SCREENSHOT, null);
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableConstraintLayout.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableConstraintLayout.kt
new file mode 100644
index 0000000..9763665
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableConstraintLayout.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.common.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.systemui.animation.LaunchableView
+import com.android.systemui.animation.LaunchableViewDelegate
+
+/** A [ConstraintLayout] that also implements [LaunchableView]. */
+open class LaunchableConstraintLayout : ConstraintLayout, LaunchableView {
+ private val delegate =
+ LaunchableViewDelegate(
+ this,
+ superSetVisibility = { super.setVisibility(it) },
+ )
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {
+ delegate.setShouldBlockVisibilityChanges(block)
+ }
+
+ override fun setVisibility(visibility: Int) {
+ delegate.setVisibility(visibility)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
index f95a8ee..7bbfec7 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt
@@ -28,7 +28,6 @@
LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
- superSetTransitionVisibility = { super.setTransitionVisibility(it) },
)
constructor(context: Context?) : super(context)
@@ -53,8 +52,4 @@
override fun setVisibility(visibility: Int) {
delegate.setVisibility(visibility)
}
-
- override fun setTransitionVisibility(visibility: Int) {
- delegate.setTransitionVisibility(visibility)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt
index c27b82a..2edac52 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt
@@ -23,12 +23,11 @@
import com.android.systemui.animation.LaunchableViewDelegate
/** A [LinearLayout] that also implements [LaunchableView]. */
-class LaunchableLinearLayout : LinearLayout, LaunchableView {
+open class LaunchableLinearLayout : LinearLayout, LaunchableView {
private val delegate =
LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
- superSetTransitionVisibility = { super.setTransitionVisibility(it) },
)
constructor(context: Context?) : super(context)
@@ -53,8 +52,4 @@
override fun setVisibility(visibility: Int) {
delegate.setVisibility(visibility)
}
-
- override fun setTransitionVisibility(visibility: Int) {
- delegate.setTransitionVisibility(visibility)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
new file mode 100644
index 0000000..2dd98dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.common.ui.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlinx.coroutines.DisposableHandle
+
+/**
+ * View designed to handle long-presses.
+ *
+ * The view will not handle any long pressed by default. To set it up, set up a listener and, when
+ * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
+ */
+class LongPressHandlingView(
+ context: Context,
+ attrs: AttributeSet?,
+) :
+ View(
+ context,
+ attrs,
+ ) {
+ interface Listener {
+ /** Notifies that a long-press has been detected by the given view. */
+ fun onLongPressDetected(
+ view: View,
+ x: Int,
+ y: Int,
+ )
+
+ /** Notifies that the gesture was too short for a long press, it is actually a click. */
+ fun onSingleTapDetected(view: View) = Unit
+ }
+
+ var listener: Listener? = null
+
+ private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
+ LongPressHandlingViewInteractionHandler(
+ postDelayed = { block, timeoutMs ->
+ val dispatchToken = Any()
+
+ handler.postDelayed(
+ block,
+ dispatchToken,
+ timeoutMs,
+ )
+
+ DisposableHandle { handler.removeCallbacksAndMessages(dispatchToken) }
+ },
+ isAttachedToWindow = ::isAttachedToWindow,
+ onLongPressDetected = { x, y ->
+ listener?.onLongPressDetected(
+ view = this,
+ x = x,
+ y = y,
+ )
+ },
+ onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) },
+ )
+ }
+
+ fun setLongPressHandlingEnabled(isEnabled: Boolean) {
+ interactionHandler.isLongPressHandlingEnabled = isEnabled
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return interactionHandler.onTouchEvent(event?.toModel())
+ }
+}
+
+private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
+ return when (actionMasked) {
+ MotionEvent.ACTION_DOWN ->
+ LongPressHandlingViewInteractionHandler.MotionEventModel.Down(
+ x = x.toInt(),
+ y = y.toInt(),
+ )
+ MotionEvent.ACTION_MOVE ->
+ LongPressHandlingViewInteractionHandler.MotionEventModel.Move(
+ distanceMoved = distanceMoved(),
+ )
+ MotionEvent.ACTION_UP ->
+ LongPressHandlingViewInteractionHandler.MotionEventModel.Up(
+ distanceMoved = distanceMoved(),
+ gestureDuration = gestureDuration(),
+ )
+ MotionEvent.ACTION_CANCEL -> LongPressHandlingViewInteractionHandler.MotionEventModel.Cancel
+ else -> LongPressHandlingViewInteractionHandler.MotionEventModel.Other
+ }
+}
+
+private fun MotionEvent.distanceMoved(): Float {
+ return if (historySize > 0) {
+ sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
+ } else {
+ 0f
+ }
+}
+
+private fun MotionEvent.gestureDuration(): Long {
+ return eventTime - downTime
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
new file mode 100644
index 0000000..c2d4d12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.common.ui.view
+
+import android.view.ViewConfiguration
+import kotlinx.coroutines.DisposableHandle
+
+/** Encapsulates logic to handle complex touch interactions with a [LongPressHandlingView]. */
+class LongPressHandlingViewInteractionHandler(
+ /**
+ * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
+ * allowing the delayed runnable to be canceled before it is run.
+ */
+ private val postDelayed: (block: Runnable, delayMs: Long) -> DisposableHandle,
+ /** Callback to be queried to check if the view is attached to its window. */
+ private val isAttachedToWindow: () -> Boolean,
+ /** Callback reporting the a long-press gesture was detected at the given coordinates. */
+ private val onLongPressDetected: (x: Int, y: Int) -> Unit,
+ /** Callback reporting the a single tap gesture was detected at the given coordinates. */
+ private val onSingleTapDetected: () -> Unit,
+) {
+ sealed class MotionEventModel {
+ object Other : MotionEventModel()
+
+ data class Down(
+ val x: Int,
+ val y: Int,
+ ) : MotionEventModel()
+
+ data class Move(
+ val distanceMoved: Float,
+ ) : MotionEventModel()
+
+ data class Up(
+ val distanceMoved: Float,
+ val gestureDuration: Long,
+ ) : MotionEventModel()
+
+ object Cancel : MotionEventModel()
+ }
+
+ var isLongPressHandlingEnabled: Boolean = false
+ var scheduledLongPressHandle: DisposableHandle? = null
+
+ fun onTouchEvent(event: MotionEventModel?): Boolean {
+ if (!isLongPressHandlingEnabled) {
+ return false
+ }
+
+ return when (event) {
+ is MotionEventModel.Down -> {
+ scheduleLongPress(event.x, event.y)
+ true
+ }
+ is MotionEventModel.Move -> {
+ if (event.distanceMoved > ViewConfiguration.getTouchSlop()) {
+ cancelScheduledLongPress()
+ }
+ false
+ }
+ is MotionEventModel.Up -> {
+ cancelScheduledLongPress()
+ if (
+ event.distanceMoved <= ViewConfiguration.getTouchSlop() &&
+ event.gestureDuration < ViewConfiguration.getLongPressTimeout()
+ ) {
+ dispatchSingleTap()
+ }
+ false
+ }
+ is MotionEventModel.Cancel -> {
+ cancelScheduledLongPress()
+ false
+ }
+ else -> false
+ }
+ }
+
+ private fun scheduleLongPress(
+ x: Int,
+ y: Int,
+ ) {
+ scheduledLongPressHandle =
+ postDelayed(
+ {
+ dispatchLongPress(
+ x = x,
+ y = y,
+ )
+ },
+ ViewConfiguration.getLongPressTimeout().toLong(),
+ )
+ }
+
+ private fun dispatchLongPress(
+ x: Int,
+ y: Int,
+ ) {
+ if (!isAttachedToWindow()) {
+ return
+ }
+
+ onLongPressDetected(x, y)
+ }
+
+ private fun cancelScheduledLongPress() {
+ scheduledLongPressHandle?.dispose()
+ }
+
+ private fun dispatchSingleTap() {
+ if (!isAttachedToWindow()) {
+ return
+ }
+
+ onSingleTapDetected()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index e5ec727..c0f8549 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -17,8 +17,12 @@
package com.android.systemui.compose
+import android.content.Context
+import android.view.View
import androidx.activity.ComponentActivity
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
/**
* A facade to interact with Compose, when it is available.
@@ -35,10 +39,22 @@
*/
fun isComposeAvailable(): Boolean
+ /**
+ * Return the [ComposeInitializer] to make Compose usable in windows outside normal activities.
+ */
+ fun composeInitializer(): ComposeInitializer
+
/** Bind the content of [activity] to [viewModel]. */
fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
onResult: (PeopleViewModel.Result) -> Unit,
)
+
+ /** Create a [View] to represent [viewModel] on screen. */
+ fun createFooterActionsView(
+ context: Context,
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner,
+ ): View
}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt
new file mode 100644
index 0000000..90dc3a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.compose
+
+import android.view.View
+
+/**
+ * An initializer to use Compose outside of an Activity, e.g. inside a window added directly using
+ * [android.view.WindowManager.addView] (like the shade or status bar) or inside a dialog.
+ *
+ * Example:
+ * ```
+ * windowManager.addView(MyWindowRootView(context), /* layoutParams */)
+ *
+ * class MyWindowRootView(context: Context) : FrameLayout(context) {
+ * override fun onAttachedToWindow() {
+ * super.onAttachedToWindow()
+ * ComposeInitializer.onAttachedToWindow(this)
+ * }
+ *
+ * override fun onDetachedFromWindow() {
+ * super.onDetachedFromWindow()
+ * ComposeInitializer.onDetachedFromWindow(this)
+ * }
+ * }
+ * ```
+ */
+interface ComposeInitializer {
+ /** Function to be called on your window root view's [View.onAttachedToWindow] function. */
+ fun onAttachedToWindow(root: View)
+
+ /** Function to be called on your window root view's [View.onDetachedFromWindow] function. */
+ fun onDetachedFromWindow(root: View)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
index eed5531..9b2a224 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt
@@ -51,13 +51,22 @@
fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback)
/**
- * Request to bind to the given service.
+ * Request to bind to the given service. This should only be used for services using the full
+ * [ControlsProviderService] API, where SystemUI renders the devices' UI.
*
* @param component The [ComponentName] of the service to bind
*/
fun bindService(component: ComponentName)
/**
+ * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+ * the app to remain "warm", and reduce latency.
+ *
+ * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+ */
+ fun bindServiceForPanel(component: ComponentName)
+
+ /**
* Send a subscribe message to retrieve status of a set of controls.
*
* @param structureInfo structure containing the controls to update
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
index 2f0fd99..3d6d335 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt
@@ -170,6 +170,10 @@
retrieveLifecycleManager(component).bindService()
}
+ override fun bindServiceForPanel(component: ComponentName) {
+ retrieveLifecycleManager(component).bindServiceForPanel()
+ }
+
override fun changeUser(newUser: UserHandle) {
if (newUser == currentUser) return
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index 2f49c3f..822190f 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -188,6 +188,16 @@
/** See [ControlsUiController.getPreferredSelectedItem]. */
fun getPreferredSelection(): SelectedItem
+ fun setPreferredSelection(selectedItem: SelectedItem)
+
+ /**
+ * Bind to a service that provides a Device Controls panel (embedded activity). This will allow
+ * the app to remain "warm", and reduce latency.
+ *
+ * @param component The [ComponentName] of the [ControlsProviderService] to bind.
+ */
+ fun bindComponentForPanel(componentName: ComponentName)
+
/**
* Interface for structure to pass data to [ControlsFavoritingActivity].
*/
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index 80c5f66..1cbfe01 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -36,6 +36,7 @@
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.dagger.SysUISingleton
@@ -61,6 +62,7 @@
private val listingController: ControlsListingController,
private val userFileManager: UserFileManager,
private val userTracker: UserTracker,
+ private val authorizedPanelsRepository: AuthorizedPanelsRepository,
optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
dumpManager: DumpManager,
) : Dumpable, ControlsController {
@@ -249,6 +251,11 @@
private fun resetFavorites() {
Favorites.clear()
Favorites.load(persistenceWrapper.readFavorites())
+ // After loading favorites, add the package names of any apps with favorites to the list
+ // of authorized panels. That way, if the user has previously favorited controls for an app,
+ // that panel will be authorized.
+ authorizedPanelsRepository.addAuthorizedPanels(
+ Favorites.getAllStructures().map { it.componentName.packageName }.toSet())
}
private fun confirmAvailability(): Boolean {
@@ -477,6 +484,10 @@
bindingController.unsubscribe()
}
+ override fun bindComponentForPanel(componentName: ComponentName) {
+ bindingController.bindServiceForPanel(componentName)
+ }
+
override fun addFavorite(
componentName: ComponentName,
structureName: CharSequence,
@@ -485,6 +496,7 @@
if (!confirmAvailability()) return
executor.execute {
if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
+ authorizedPanelsRepository.addAuthorizedPanels(setOf(componentName.packageName))
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
}
@@ -551,6 +563,10 @@
return uiController.getPreferredSelectedItem(getFavorites())
}
+ override fun setPreferredSelection(selectedItem: SelectedItem) {
+ uiController.updatePreferences(selectedItem)
+ }
+
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:")
pw.println(" Changing users: $userChanging")
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
index 5b38e5b..72c3a94 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt
@@ -78,6 +78,10 @@
private const val DEBUG = true
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
Context.BIND_NOT_PERCEPTIBLE
+ // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI.
+ // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app
+ // once the Task is finished in the device controls panel.
+ private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE
}
private val intent = Intent().apply {
@@ -87,18 +91,19 @@
})
}
- private fun bindService(bind: Boolean) {
+ private fun bindService(bind: Boolean, forPanel: Boolean = false) {
executor.execute {
requiresBound = bind
if (bind) {
- if (bindTryCount != MAX_BIND_RETRIES) {
+ if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) {
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
+ val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
val bound = context
- .bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user)
+ .bindServiceAsUser(intent, serviceConnection, flags, user)
if (!bound) {
context.unbindService(serviceConnection)
}
@@ -279,6 +284,10 @@
bindService(true)
}
+ fun bindServiceForPanel() {
+ bindService(bind = true, forPanel = true)
+ }
+
/**
* Request unbind from the service.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index 6d6410d..6af8e73 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -34,6 +34,8 @@
import com.android.systemui.controls.management.ControlsListingControllerImpl
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
import com.android.systemui.controls.management.ControlsRequestDialog
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.AuthorizedPanelsRepositoryImpl
import com.android.systemui.controls.settings.ControlsSettingsDialogManager
import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl
import com.android.systemui.controls.ui.ControlActionCoordinator
@@ -104,6 +106,11 @@
coordinator: ControlActionCoordinatorImpl
): ControlActionCoordinator
+ @Binds
+ abstract fun provideAuthorizedPanelsRepository(
+ repository: AuthorizedPanelsRepositoryImpl
+ ): AuthorizedPanelsRepository
+
@BindsOptionalOf
abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
index 753d5ad..3fe0f03 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt
@@ -45,14 +45,15 @@
* @param onAppSelected a callback to indicate that an app has been selected in the list.
*/
class AppAdapter(
- backgroundExecutor: Executor,
- uiExecutor: Executor,
- lifecycle: Lifecycle,
- controlsListingController: ControlsListingController,
- private val layoutInflater: LayoutInflater,
- private val onAppSelected: (ComponentName?) -> Unit = {},
- private val favoritesRenderer: FavoritesRenderer,
- private val resources: Resources
+ backgroundExecutor: Executor,
+ uiExecutor: Executor,
+ lifecycle: Lifecycle,
+ controlsListingController: ControlsListingController,
+ private val layoutInflater: LayoutInflater,
+ private val onAppSelected: (ControlsServiceInfo) -> Unit = {},
+ private val favoritesRenderer: FavoritesRenderer,
+ private val resources: Resources,
+ private val authorizedPanels: Set<String> = emptySet(),
) : RecyclerView.Adapter<AppAdapter.Holder>() {
private var listOfServices = emptyList<ControlsServiceInfo>()
@@ -64,8 +65,10 @@
val localeComparator = compareBy<ControlsServiceInfo, CharSequence>(collator) {
it.loadLabel() ?: ""
}
- listOfServices = serviceInfos.filter { it.panelActivity == null }
- .sortedWith(localeComparator)
+ // No panel or the panel is not authorized
+ listOfServices = serviceInfos.filter {
+ it.panelActivity == null || it.panelActivity?.packageName !in authorizedPanels
+ }.sortedWith(localeComparator)
uiExecutor.execute(::notifyDataSetChanged)
}
}
@@ -86,8 +89,8 @@
override fun onBindViewHolder(holder: Holder, index: Int) {
holder.bindData(listOfServices[index])
- holder.itemView.setOnClickListener {
- onAppSelected(ComponentName.unflattenFromString(listOfServices[index].key))
+ holder.view.setOnClickListener {
+ onAppSelected(listOfServices[index])
}
}
@@ -95,6 +98,8 @@
* Holder for binding views in the [RecyclerView]-
*/
class Holder(view: View, val favRenderer: FavoritesRenderer) : RecyclerView.ViewHolder(view) {
+ val view: View = itemView
+
private val icon: ImageView = itemView.requireViewById(com.android.internal.R.id.icon)
private val title: TextView = itemView.requireViewById(com.android.internal.R.id.title)
private val favorites: TextView = itemView.requireViewById(R.id.favorites)
@@ -106,7 +111,11 @@
fun bindData(data: ControlsServiceInfo) {
icon.setImageDrawable(data.loadIcon())
title.text = data.loadLabel()
- val text = favRenderer.renderFavoritesForComponent(data.componentName)
+ val text = if (data.panelActivity == null) {
+ favRenderer.renderFavoritesForComponent(data.componentName)
+ } else {
+ null
+ }
favorites.text = text
favorites.visibility = if (text == null) View.GONE else View.VISIBLE
}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
index 90bc5d0..3808e73 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -17,6 +17,7 @@
package com.android.systemui.controls.management
import android.app.ActivityOptions
+import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -31,12 +32,15 @@
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.activity.ComponentActivity
+import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
+import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.controls.ui.ControlsActivity
-import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.controls.ui.SelectedItem
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.UserTracker
@@ -52,7 +56,8 @@
private val listingController: ControlsListingController,
private val controlsController: ControlsController,
private val userTracker: UserTracker,
- private val uiController: ControlsUiController
+ private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+ private val panelConfirmationDialogFactory: PanelConfirmationDialogFactory
) : ComponentActivity() {
companion object {
@@ -72,6 +77,7 @@
}
}
}
+ private var dialog: Dialog? = null
private val mOnBackInvokedCallback = OnBackInvokedCallback {
if (DEBUG) {
@@ -138,9 +144,11 @@
lifecycle,
listingController,
LayoutInflater.from(this),
- ::launchFavoritingActivity,
+ ::onAppSelected,
FavoritesRenderer(resources, controlsController::countFavoritesForComponent),
- resources).apply {
+ resources,
+ authorizedPanelsRepository.getAuthorizedPanels()
+ ).apply {
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
var hasAnimated = false
override fun onChanged() {
@@ -167,13 +175,35 @@
Log.d(TAG, "Unregistered onBackInvokedCallback")
}
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback)
+ dialog?.cancel()
+ }
+
+ fun onAppSelected(serviceInfo: ControlsServiceInfo) {
+ dialog?.cancel()
+ if (serviceInfo.panelActivity == null) {
+ launchFavoritingActivity(serviceInfo.componentName)
+ } else {
+ val appName = serviceInfo.loadLabel() ?: ""
+ dialog = panelConfirmationDialogFactory.createConfirmationDialog(this, appName) { ok ->
+ if (ok) {
+ authorizedPanelsRepository.addAuthorizedPanels(
+ setOf(serviceInfo.componentName.packageName)
+ )
+ val selected = SelectedItem.PanelItem(appName, componentName)
+ controlsController.setPreferredSelection(selected)
+ animateExitAndFinish()
+ openControlsOrigin()
+ }
+ dialog = null
+ }.also { it.show() }
+ }
}
/**
* Launch the [ControlsFavoritingActivity] for the specified component.
* @param component a component name for a [ControlsProviderService]
*/
- fun launchFavoritingActivity(component: ComponentName?) {
+ private fun launchFavoritingActivity(component: ComponentName?) {
executor.execute {
component?.let {
val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java)
@@ -194,7 +224,15 @@
super.onDestroy()
}
- private fun animateExitAndFinish() {
+ private fun openControlsOrigin() {
+ startActivity(
+ Intent(applicationContext, ControlsActivity::class.java),
+ ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ )
+ }
+
+ @VisibleForTesting
+ internal open fun animateExitAndFinish() {
val rootView = requireViewById<ViewGroup>(R.id.controls_management_root)
ControlsAnimations.exitAnimation(
rootView,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
new file mode 100644
index 0000000..6f87aa9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.controls.management
+
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * Factory to create dialogs for consenting to show app panels for specific apps.
+ *
+ * [internalDialogFactory] is for facilitating testing.
+ */
+class PanelConfirmationDialogFactory(
+ private val internalDialogFactory: (Context) -> SystemUIDialog
+) {
+ @Inject constructor() : this({ SystemUIDialog(it) })
+
+ /**
+ * Creates a dialog to show to the user. [response] will be true if an only if the user responds
+ * affirmatively.
+ */
+ fun createConfirmationDialog(
+ context: Context,
+ appName: CharSequence,
+ response: Consumer<Boolean>
+ ): Dialog {
+ val listener =
+ DialogInterface.OnClickListener { _, which ->
+ response.accept(which == DialogInterface.BUTTON_POSITIVE)
+ }
+ return internalDialogFactory(context).apply {
+ setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName))
+ setMessage(this.context.getString(R.string.controls_panel_authorization, appName))
+ setCanceledOnTouchOutside(true)
+ setOnCancelListener { response.accept(false) }
+ setPositiveButton(R.string.controls_dialog_ok, listener)
+ setNeutralButton(R.string.cancel, listener)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt
new file mode 100644
index 0000000..3e672f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.controls.panels
+
+/**
+ * Repository for keeping track of which packages the panel has authorized to show control panels
+ * (embedded activity).
+ */
+interface AuthorizedPanelsRepository {
+
+ /** A set of package names that the user has previously authorized to show panels. */
+ fun getAuthorizedPanels(): Set<String>
+
+ /** Adds [packageNames] to the set of packages that the user has authorized to show panels. */
+ fun addAuthorizedPanels(packageNames: Set<String>)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
new file mode 100644
index 0000000..f7e43a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.controls.panels
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.android.systemui.R
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import javax.inject.Inject
+
+class AuthorizedPanelsRepositoryImpl
+@Inject
+constructor(
+ private val context: Context,
+ private val userFileManager: UserFileManager,
+ private val userTracker: UserTracker
+) : AuthorizedPanelsRepository {
+
+ override fun getAuthorizedPanels(): Set<String> {
+ return getAuthorizedPanelsInternal(instantiateSharedPrefs())
+ }
+
+ override fun addAuthorizedPanels(packageNames: Set<String>) {
+ addAuthorizedPanelsInternal(instantiateSharedPrefs(), packageNames)
+ }
+
+ private fun getAuthorizedPanelsInternal(sharedPreferences: SharedPreferences): Set<String> {
+ return sharedPreferences.getStringSet(KEY, emptySet())!!
+ }
+
+ private fun addAuthorizedPanelsInternal(
+ sharedPreferences: SharedPreferences,
+ packageNames: Set<String>
+ ) {
+ val currentSet = getAuthorizedPanelsInternal(sharedPreferences)
+ sharedPreferences.edit().putStringSet(KEY, currentSet + packageNames).apply()
+ }
+
+ private fun instantiateSharedPrefs(): SharedPreferences {
+ val sharedPref =
+ userFileManager.getSharedPreferences(
+ DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+ Context.MODE_PRIVATE,
+ userTracker.userId,
+ )
+
+ // If we've never run this (i.e., the key doesn't exist), add the default packages
+ if (sharedPref.getStringSet(KEY, null) == null) {
+ sharedPref
+ .edit()
+ .putStringSet(
+ KEY,
+ context.resources
+ .getStringArray(R.array.config_controlsPreferredPackages)
+ .toSet()
+ )
+ .apply()
+ }
+ return sharedPref
+ }
+
+ companion object {
+ private const val KEY = "authorized_panels"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index f5c5905..c1cec9d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -58,6 +58,8 @@
* This element will be the one that appears when the user first opens the controls activity.
*/
fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem
+
+ fun updatePreferences(selectedItem: SelectedItem)
}
sealed class SelectedItem {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index 1e3e5cd..9e71bef 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -25,6 +25,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.service.controls.Control
@@ -38,6 +39,7 @@
import android.view.animation.DecelerateInterpolator
import android.widget.AdapterView
import android.widget.ArrayAdapter
+import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
@@ -60,10 +62,13 @@
import com.android.systemui.controls.management.ControlsFavoritingActivity
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.globalactions.GlobalActionsPopupMenu
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
@@ -87,6 +92,7 @@
class ControlsUiControllerImpl @Inject constructor (
val controlsController: Lazy<ControlsController>,
val context: Context,
+ private val packageManager: PackageManager,
@Main val uiExecutor: DelayableExecutor,
@Background val bgExecutor: DelayableExecutor,
val controlsListingController: Lazy<ControlsListingController>,
@@ -99,6 +105,8 @@
private val userTracker: UserTracker,
private val taskViewFactory: Optional<TaskViewFactory>,
private val controlsSettingsRepository: ControlsSettingsRepository,
+ private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+ private val featureFlags: FeatureFlags,
dumpManager: DumpManager
) : ControlsUiController, Dumpable {
@@ -108,6 +116,11 @@
private const val PREF_IS_PANEL = "controls_is_panel"
private const val FADE_IN_MILLIS = 200L
+
+ private const val OPEN_APP_ID = 0L
+ private const val ADD_CONTROLS_ID = 1L
+ private const val ADD_APP_ID = 2L
+ private const val EDIT_CONTROLS_ID = 3L
}
private var selectedItem: SelectedItem = SelectedItem.EMPTY_SELECTION
@@ -135,6 +148,9 @@
it.getTitle()
}
+ private var openAppIntent: Intent? = null
+ private var overflowMenuAdapter: BaseAdapter? = null
+
private val onSeedingComplete = Consumer<Boolean> {
accepted ->
if (accepted) {
@@ -160,6 +176,7 @@
): ControlsListingController.ControlsListingCallback {
return object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+ val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
val lastItems = serviceInfos.map {
val uid = it.serviceInfo.applicationInfo.uid
@@ -169,7 +186,11 @@
it.loadIcon(),
it.componentName,
uid,
- it.panelActivity
+ if (it.componentName.packageName in authorizedPanels) {
+ it.panelActivity
+ } else {
+ null
+ }
)
}
uiExecutor.execute {
@@ -206,6 +227,8 @@
this.parent = parent
this.onDismiss = onDismiss
this.activityContext = activityContext
+ this.openAppIntent = null
+ this.overflowMenuAdapter = null
hidden = false
retainCache = false
@@ -232,6 +255,8 @@
ControlKey(selected.structure.componentName, it.ci.controlId)
}
controlsController.get().subscribeToFavorites(selected.structure)
+ } else {
+ controlsController.get().bindComponentForPanel(selected.componentName)
}
listingCallback = createCallback(::showControlsView)
}
@@ -294,6 +319,12 @@
startTargetedActivity(si, ControlsEditingActivity::class.java)
}
+ private fun startDefaultActivity() {
+ openAppIntent?.let {
+ startActivity(it, animateExtra = false)
+ }
+ }
+
private fun startTargetedActivity(si: StructureInfo, klazz: Class<*>) {
val i = Intent(activityContext, klazz)
putIntentExtras(i, si)
@@ -317,9 +348,11 @@
startActivity(i)
}
- private fun startActivity(intent: Intent) {
+ private fun startActivity(intent: Intent, animateExtra: Boolean = true) {
// Force animations when transitioning from a dialog to an activity
- intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+ if (animateExtra) {
+ intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+ }
if (keyguardStateController.isShowing()) {
activityStarter.postStartActivityDismissingKeyguard(intent, 0 /* delay */)
@@ -371,8 +404,31 @@
Log.w(ControlsUiController.TAG, "Not TaskViewFactory to display panel $selectionItem")
}
+ bgExecutor.execute {
+ val intent = Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setPackage(selectionItem.componentName.packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+ val intents = packageManager
+ .queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L))
+ intents.firstOrNull { it.activityInfo.exported }?.let { resolved ->
+ intent.setPackage(null)
+ intent.setComponent(resolved.activityInfo.componentName)
+ openAppIntent = intent
+ parent.post {
+ // This will call show on the PopupWindow in the same thread, so make sure this
+ // happens in the view thread.
+ overflowMenuAdapter?.notifyDataSetChanged()
+ }
+ }
+ }
createDropDown(panelsAndStructures, selectionItem)
- createMenu()
+
+ val currentApps = panelsAndStructures.map { it.componentName }.toSet()
+ val allApps = controlsListingController.get()
+ .getCurrentServices().map { it.componentName }.toSet()
+ createMenu(extraApps = (allApps - currentApps).isNotEmpty())
}
private fun createPanelView(componentName: ComponentName) {
@@ -411,22 +467,41 @@
}
}
- private fun createMenu() {
+ private fun createMenu(extraApps: Boolean) {
val isPanel = selectedItem is SelectedItem.PanelItem
val selectedStructure = (selectedItem as? SelectedItem.StructureItem)?.structure
?: EMPTY_STRUCTURE
+ val newFlows = featureFlags.isEnabled(Flags.CONTROLS_MANAGEMENT_NEW_FLOWS)
- val items = if (isPanel) {
- arrayOf(
- context.resources.getString(R.string.controls_menu_add),
- )
- } else {
- arrayOf(
- context.resources.getString(R.string.controls_menu_add),
- context.resources.getString(R.string.controls_menu_edit)
- )
+ val items = buildList {
+ add(OverflowMenuAdapter.MenuItem(
+ context.getText(R.string.controls_open_app),
+ OPEN_APP_ID
+ ))
+ if (newFlows || isPanel) {
+ if (extraApps) {
+ add(OverflowMenuAdapter.MenuItem(
+ context.getText(R.string.controls_menu_add_another_app),
+ ADD_APP_ID
+ ))
+ }
+ } else {
+ add(OverflowMenuAdapter.MenuItem(
+ context.getText(R.string.controls_menu_add),
+ ADD_CONTROLS_ID
+ ))
+ }
+ if (!isPanel) {
+ add(OverflowMenuAdapter.MenuItem(
+ context.getText(R.string.controls_menu_edit),
+ EDIT_CONTROLS_ID
+ ))
+ }
}
- var adapter = ArrayAdapter<String>(context, R.layout.controls_more_item, items)
+
+ val adapter = OverflowMenuAdapter(context, R.layout.controls_more_item, items) { position ->
+ getItemId(position) != OPEN_APP_ID || openAppIntent != null
+ }
val anchor = parent.requireViewById<ImageView>(R.id.controls_more)
anchor.setOnClickListener(object : View.OnClickListener {
@@ -444,25 +519,21 @@
pos: Int,
id: Long
) {
- when (pos) {
- // 0: Add Control
- 0 -> {
- if (isPanel) {
- startProviderSelectorActivity()
- } else {
- startFavoritingActivity(selectedStructure)
- }
- }
- // 1: Edit controls
- 1 -> startEditingActivity(selectedStructure)
+ when (id) {
+ OPEN_APP_ID -> startDefaultActivity()
+ ADD_APP_ID -> startProviderSelectorActivity()
+ ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure)
+ EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure)
}
dismiss()
}
})
show()
+ listView?.post { listView?.requestAccessibilityFocus() }
}
}
})
+ overflowMenuAdapter = adapter
}
private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
@@ -524,6 +595,7 @@
}
})
show()
+ listView?.post { listView?.requestAccessibilityFocus() }
}
}
})
@@ -608,12 +680,12 @@
}
}
- private fun updatePreferences(si: SelectedItem) {
+ override fun updatePreferences(selectedItem: SelectedItem) {
sharedPreferences.edit()
- .putString(PREF_COMPONENT, si.componentName.flattenToString())
- .putString(PREF_STRUCTURE_OR_APP_NAME, si.name.toString())
- .putBoolean(PREF_IS_PANEL, si is SelectedItem.PanelItem)
- .commit()
+ .putString(PREF_COMPONENT, selectedItem.componentName.flattenToString())
+ .putString(PREF_STRUCTURE_OR_APP_NAME, selectedItem.name.toString())
+ .putBoolean(PREF_IS_PANEL, selectedItem is SelectedItem.PanelItem)
+ .apply()
}
private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/OverflowMenuAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/OverflowMenuAdapter.kt
new file mode 100644
index 0000000..6b84e36
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/OverflowMenuAdapter.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.controls.ui
+
+import android.content.Context
+import android.widget.ArrayAdapter
+import androidx.annotation.LayoutRes
+
+open class OverflowMenuAdapter(
+ context: Context,
+ @LayoutRes layoutId: Int,
+ itemsWithIds: List<MenuItem>,
+ private val isEnabledInternal: OverflowMenuAdapter.(Int) -> Boolean
+) : ArrayAdapter<CharSequence>(context, layoutId, itemsWithIds.map(MenuItem::text)) {
+
+ private val ids = itemsWithIds.map(MenuItem::id)
+
+ override fun getItemId(position: Int): Long {
+ return ids[position]
+ }
+
+ override fun isEnabled(position: Int): Boolean {
+ return isEnabledInternal(position)
+ }
+
+ data class MenuItem(val text: CharSequence, val id: Long)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 8012dea..2dfcf70 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -27,10 +27,13 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.clipboardoverlay.ClipboardListener
import com.android.systemui.dagger.qualifiers.PerUser
+import com.android.systemui.dreams.DreamMonitor
import com.android.systemui.globalactions.GlobalActionsComponent
import com.android.systemui.keyboard.KeyboardUI
import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.data.quickaffordance.MuteQuickAffordanceCoreStartable
import com.android.systemui.log.SessionTracker
+import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
import com.android.systemui.media.RingtonePlayer
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
@@ -217,6 +220,12 @@
@ClassKey(ToastUI::class)
abstract fun bindToastUI(service: ToastUI): CoreStartable
+ /** Inject into MediaOutputSwitcherDialogUI. */
+ @Binds
+ @IntoMap
+ @ClassKey(MediaOutputSwitcherDialogUI::class)
+ abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable
+
/** Inject into VolumeUI. */
@Binds
@IntoMap
@@ -279,4 +288,18 @@
@IntoMap
@ClassKey(StylusUsiPowerStartable::class)
abstract fun bindStylusUsiPowerStartable(sysui: StylusUsiPowerStartable): CoreStartable
+
+ /** Inject into MuteQuickAffordanceCoreStartable*/
+ @Binds
+ @IntoMap
+ @ClassKey(MuteQuickAffordanceCoreStartable::class)
+ abstract fun bindMuteQuickAffordanceCoreStartable(
+ sysui: MuteQuickAffordanceCoreStartable
+ ): CoreStartable
+
+ /**Inject into DreamMonitor */
+ @Binds
+ @IntoMap
+ @ClassKey(DreamMonitor::class)
+ abstract fun bindDreamMonitor(sysui: DreamMonitor): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index b8e6673..6274a26 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -31,6 +31,7 @@
import com.android.systemui.appops.dagger.AppOpsModule;
import com.android.systemui.assist.AssistModule;
import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
import com.android.systemui.biometrics.dagger.BiometricsModule;
import com.android.systemui.biometrics.dagger.UdfpsModule;
@@ -53,6 +54,7 @@
import com.android.systemui.navigationbar.NavigationBarComponent;
import com.android.systemui.notetask.NoteTaskModule;
import com.android.systemui.people.PeopleModule;
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin;
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
import com.android.systemui.privacy.PrivacyModule;
import com.android.systemui.qs.FgsManagerController;
@@ -61,6 +63,7 @@
import com.android.systemui.recents.Recents;
import com.android.systemui.screenshot.dagger.ScreenshotModule;
import com.android.systemui.security.data.repository.SecurityRepositoryModule;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.smartspace.dagger.SmartspaceModule;
@@ -105,6 +108,8 @@
import java.util.Optional;
import java.util.concurrent.Executor;
+import javax.inject.Named;
+
import dagger.Binds;
import dagger.BindsOptionalOf;
import dagger.Module;
@@ -194,8 +199,8 @@
@SysUISingleton
@Provides
- static SysUiState provideSysUiState(DumpManager dumpManager) {
- final SysUiState state = new SysUiState();
+ static SysUiState provideSysUiState(DisplayTracker displayTracker, DumpManager dumpManager) {
+ final SysUiState state = new SysUiState(displayTracker);
dumpManager.registerDumpable(state);
return state;
}
@@ -210,6 +215,17 @@
abstract BcSmartspaceDataPlugin optionalBcSmartspaceDataPlugin();
@BindsOptionalOf
+ abstract BcSmartspaceConfigPlugin optionalBcSmartspaceConfigPlugin();
+
+ @BindsOptionalOf
+ @Named(SmartspaceModule.DATE_SMARTSPACE_DATA_PLUGIN)
+ abstract BcSmartspaceDataPlugin optionalDateSmartspaceConfigPlugin();
+
+ @BindsOptionalOf
+ @Named(SmartspaceModule.WEATHER_SMARTSPACE_DATA_PLUGIN)
+ abstract BcSmartspaceDataPlugin optionalWeatherSmartspaceConfigPlugin();
+
+ @BindsOptionalOf
abstract Recents optionalRecents();
@BindsOptionalOf
@@ -221,6 +237,9 @@
@BindsOptionalOf
abstract AlternateUdfpsTouchProvider optionalUdfpsTouchProvider();
+ @BindsOptionalOf
+ abstract FingerprintInteractiveToAuthProvider optionalFingerprintInteractiveToAuthProvider();
+
@SysUISingleton
@Binds
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 976afd4..88c0c50 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,6 +34,7 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -45,6 +46,7 @@
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@Main private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
) : DecorProviderFactory() {
private val display = context.display
private val displayInfo = DisplayInfo()
@@ -82,7 +84,8 @@
authController,
statusBarStateController,
keyguardUpdateMonitor,
- mainExecutor
+ mainExecutor,
+ logger,
)
)
}
@@ -104,7 +107,8 @@
private val authController: AuthController,
private val statusBarStateController: StatusBarStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val mainExecutor: Executor
+ private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
) : BoundDecorProvider() {
override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
@@ -136,7 +140,8 @@
alignedBound,
statusBarStateController,
keyguardUpdateMonitor,
- mainExecutor
+ mainExecutor,
+ logger,
)
view.id = viewId
view.setColor(tintColor)
@@ -155,8 +160,9 @@
layoutParams.let { lp ->
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+ logger.faceSensorLocation(authController.faceSensorLocation)
authController.faceSensorLocation?.y?.let { faceAuthSensorHeight ->
- val faceScanningHeight = (faceAuthSensorHeight * 2).toInt()
+ val faceScanningHeight = (faceAuthSensorHeight * 2)
when (rotation) {
Surface.ROTATION_0, Surface.ROTATION_180 ->
lp.height = faceScanningHeight
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
index 2a3d67f..c331164 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeDockHandler.java
@@ -17,12 +17,12 @@
package com.android.systemui.doze;
import android.hardware.display.AmbientDisplayConfiguration;
-import android.os.UserHandle;
import android.util.Log;
import com.android.systemui.dock.DockManager;
import com.android.systemui.doze.DozeMachine.State;
import com.android.systemui.doze.dagger.DozeScope;
+import com.android.systemui.settings.UserTracker;
import java.io.PrintWriter;
@@ -40,14 +40,17 @@
private final AmbientDisplayConfiguration mConfig;
private DozeMachine mMachine;
private final DockManager mDockManager;
+ private final UserTracker mUserTracker;
private final DockEventListener mDockEventListener;
private int mDockState = DockManager.STATE_NONE;
@Inject
- DozeDockHandler(AmbientDisplayConfiguration config, DockManager dockManager) {
+ DozeDockHandler(AmbientDisplayConfiguration config, DockManager dockManager,
+ UserTracker userTracker) {
mConfig = config;
mDockManager = dockManager;
+ mUserTracker = userTracker;
mDockEventListener = new DockEventListener();
}
@@ -100,7 +103,7 @@
nextState = State.DOZE_AOD_DOCKED;
break;
case DockManager.STATE_NONE:
- nextState = mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) ? State.DOZE_AOD
+ nextState = mConfig.alwaysOnEnabled(mUserTracker.getUserId()) ? State.DOZE_AOD
: State.DOZE;
break;
case DockManager.STATE_DOCKED_HIDE:
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index 96c35d4..fc3263f 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -23,7 +23,6 @@
import android.content.res.Configuration;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Trace;
-import android.os.UserHandle;
import android.util.Log;
import android.view.Display;
@@ -33,6 +32,7 @@
import com.android.systemui.doze.dagger.WrappedService;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.util.Assert;
import com.android.systemui.util.wakelock.WakeLock;
@@ -149,6 +149,7 @@
private final DozeHost mDozeHost;
private final DockManager mDockManager;
private final Part[] mParts;
+ private final UserTracker mUserTracker;
private final ArrayList<State> mQueuedRequests = new ArrayList<>();
private State mState = State.UNINITIALIZED;
@@ -161,7 +162,7 @@
AmbientDisplayConfiguration ambientDisplayConfig,
WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
DozeLog dozeLog, DockManager dockManager,
- DozeHost dozeHost, Part[] parts) {
+ DozeHost dozeHost, Part[] parts, UserTracker userTracker) {
mDozeService = service;
mAmbientDisplayConfig = ambientDisplayConfig;
mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -170,6 +171,7 @@
mDockManager = dockManager;
mDozeHost = dozeHost;
mParts = parts;
+ mUserTracker = userTracker;
for (Part part : parts) {
part.setDozeMachine(this);
}
@@ -429,7 +431,7 @@
nextState = State.FINISH;
} else if (mDockManager.isDocked()) {
nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED;
- } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+ } else if (mAmbientDisplayConfig.alwaysOnEnabled(mUserTracker.getUserId())) {
nextState = State.DOZE_AOD;
} else {
nextState = State.DOZE;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 937884c..4cade77 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -43,6 +43,7 @@
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.util.sensors.AsyncSensorManager;
+import com.android.systemui.util.settings.SystemSettings;
import java.io.PrintWriter;
import java.util.Objects;
@@ -78,6 +79,7 @@
private final DozeParameters mDozeParameters;
private final DevicePostureController mDevicePostureController;
private final DozeLog mDozeLog;
+ private final SystemSettings mSystemSettings;
private final int[] mSensorToBrightness;
private final int[] mSensorToScrimOpacity;
private final int mScreenBrightnessDim;
@@ -110,7 +112,8 @@
WakefulnessLifecycle wakefulnessLifecycle,
DozeParameters dozeParameters,
DevicePostureController devicePostureController,
- DozeLog dozeLog) {
+ DozeLog dozeLog,
+ SystemSettings systemSettings) {
mContext = context;
mDozeService = service;
mSensorManager = sensorManager;
@@ -122,6 +125,7 @@
mDozeHost = host;
mHandler = handler;
mDozeLog = dozeLog;
+ mSystemSettings = systemSettings;
mScreenBrightnessMinimumDimAmountFloat = context.getResources().getFloat(
R.dimen.config_screenBrightnessMinimumDimAmountFloat);
@@ -257,7 +261,7 @@
}
//TODO: brightnessfloat change usages to float.
private int clampToUserSetting(int brightness) {
- int userSetting = Settings.System.getIntForUser(mContext.getContentResolver(),
+ int userSetting = mSystemSettings.getIntForUser(
Settings.System.SCREEN_BRIGHTNESS, Integer.MAX_VALUE,
UserHandle.USER_CURRENT);
return Math.min(brightness, userSetting);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 37183e8..e3c568a9 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -116,6 +116,7 @@
private boolean mListening;
private boolean mListeningTouchScreenSensors;
private boolean mListeningProxSensors;
+ private boolean mListeningAodOnlySensors;
private boolean mUdfpsEnrolled;
@DevicePostureController.DevicePostureInt
@@ -184,7 +185,8 @@
dozeParameters.getPulseOnSigMotion(),
DozeLog.PULSE_REASON_SENSOR_SIGMOTION,
false /* touchCoords */,
- false /* touchscreen */),
+ false /* touchscreen */
+ ),
new TriggerSensor(
mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
Settings.Secure.DOZE_PICK_UP_GESTURE,
@@ -195,14 +197,17 @@
false /* touchscreen */,
false /* ignoresSetting */,
false /* requires prox */,
- true /* immediatelyReRegister */),
+ true /* immediatelyReRegister */,
+ false /* requiresAod */
+ ),
new TriggerSensor(
findSensor(config.doubleTapSensorType()),
Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
true /* configured */,
DozeLog.REASON_SENSOR_DOUBLE_TAP,
dozeParameters.doubleTapReportsTouchCoordinates(),
- true /* touchscreen */),
+ true /* touchscreen */
+ ),
new TriggerSensor(
findSensors(config.tapSensorTypeMapping()),
Settings.Secure.DOZE_TAP_SCREEN_GESTURE,
@@ -214,7 +219,9 @@
false /* ignoresSetting */,
dozeParameters.singleTapUsesProx(mDevicePosture) /* requiresProx */,
true /* immediatelyReRegister */,
- mDevicePosture),
+ mDevicePosture,
+ false
+ ),
new TriggerSensor(
findSensor(config.longPressSensorType()),
Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
@@ -225,7 +232,9 @@
true /* touchscreen */,
false /* ignoresSetting */,
dozeParameters.longPressUsesProx() /* requiresProx */,
- true /* immediatelyReRegister */),
+ true /* immediatelyReRegister */,
+ false /* requiresAod */
+ ),
new TriggerSensor(
findSensor(config.udfpsLongPressSensorType()),
"doze_pulse_on_auth",
@@ -236,15 +245,18 @@
true /* touchscreen */,
false /* ignoresSetting */,
dozeParameters.longPressUsesProx(),
- false /* immediatelyReRegister */),
+ false /* immediatelyReRegister */,
+ true /* requiresAod */
+ ),
new PluginSensor(
new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE,
mConfig.wakeScreenGestureAvailable()
- && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT),
+ && mConfig.alwaysOnEnabled(mUserTracker.getUserId()),
DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
false /* reports touch coordinates */,
- false /* touchscreen */),
+ false /* touchscreen */
+ ),
new PluginSensor(
new SensorManagerPlugin.Sensor(TYPE_WAKE_LOCK_SCREEN),
Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE,
@@ -252,7 +264,8 @@
DozeLog.PULSE_REASON_SENSOR_WAKE_REACH,
false /* reports touch coordinates */,
false /* touchscreen */,
- mConfig.getWakeLockScreenDebounce()),
+ mConfig.getWakeLockScreenDebounce()
+ ),
new TriggerSensor(
findSensor(config.quickPickupSensorType()),
Settings.Secure.DOZE_QUICK_PICKUP_GESTURE,
@@ -263,7 +276,9 @@
false /* requiresTouchscreen */,
false /* ignoresSetting */,
false /* requiresProx */,
- true /* immediatelyReRegister */),
+ true /* immediatelyReRegister */,
+ false /* requiresAod */
+ ),
};
setProxListening(false); // Don't immediately start listening when we register.
mProximitySensor.register(
@@ -278,7 +293,7 @@
private boolean udfpsLongPressConfigured() {
return mUdfpsEnrolled
- && (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) || mScreenOffUdfpsEnabled);
+ && (mConfig.alwaysOnEnabled(mUserTracker.getUserId()) || mScreenOffUdfpsEnabled);
}
private boolean quickPickUpConfigured() {
@@ -357,29 +372,36 @@
/**
* If sensors should be registered and sending signals.
*/
- public void setListening(boolean listen, boolean includeTouchScreenSensors) {
- if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors) {
+ public void setListening(boolean listen, boolean includeTouchScreenSensors,
+ boolean includeAodOnlySensors) {
+ if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors
+ && mListeningAodOnlySensors == includeAodOnlySensors) {
return;
}
mListening = listen;
mListeningTouchScreenSensors = includeTouchScreenSensors;
+ mListeningAodOnlySensors = includeAodOnlySensors;
updateListening();
}
/**
* If sensors should be registered and sending signals.
*/
- public void setListening(boolean listen, boolean includeTouchScreenSensors,
- boolean lowPowerStateOrOff) {
+ public void setListeningWithPowerState(boolean listen, boolean includeTouchScreenSensors,
+ boolean includeAodRequiringSensors, boolean lowPowerStateOrOff) {
final boolean shouldRegisterProxSensors =
!mSelectivelyRegisterProxSensors || lowPowerStateOrOff;
- if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors
- && mListeningProxSensors == shouldRegisterProxSensors) {
+ if (mListening == listen
+ && mListeningTouchScreenSensors == includeTouchScreenSensors
+ && mListeningProxSensors == shouldRegisterProxSensors
+ && mListeningAodOnlySensors == includeAodRequiringSensors
+ ) {
return;
}
mListening = listen;
mListeningTouchScreenSensors = includeTouchScreenSensors;
mListeningProxSensors = shouldRegisterProxSensors;
+ mListeningAodOnlySensors = includeAodRequiringSensors;
updateListening();
}
@@ -391,7 +413,8 @@
for (TriggerSensor s : mTriggerSensors) {
boolean listen = mListening
&& (!s.mRequiresTouchscreen || mListeningTouchScreenSensors)
- && (!s.mRequiresProx || mListeningProxSensors);
+ && (!s.mRequiresProx || mListeningProxSensors)
+ && (!s.mRequiresAod || mListeningAodOnlySensors);
s.setListening(listen);
if (listen) {
anyListening = true;
@@ -499,6 +522,9 @@
private final boolean mRequiresTouchscreen;
private final boolean mRequiresProx;
+ // Whether the sensor should only register if the device is in AOD
+ private final boolean mRequiresAod;
+
// Whether to immediately re-register this sensor after the sensor is triggered.
// If false, the sensor registration will be updated on the next AOD state transition.
private final boolean mImmediatelyReRegister;
@@ -527,7 +553,8 @@
requiresTouchscreen,
false /* ignoresSetting */,
false /* requiresProx */,
- true /* immediatelyReRegister */
+ true /* immediatelyReRegister */,
+ false
);
}
@@ -541,7 +568,8 @@
boolean requiresTouchscreen,
boolean ignoresSetting,
boolean requiresProx,
- boolean immediatelyReRegister
+ boolean immediatelyReRegister,
+ boolean requiresAod
) {
this(
new Sensor[]{ sensor },
@@ -554,7 +582,8 @@
ignoresSetting,
requiresProx,
immediatelyReRegister,
- DevicePostureController.DEVICE_POSTURE_UNKNOWN
+ DevicePostureController.DEVICE_POSTURE_UNKNOWN,
+ requiresAod
);
}
@@ -569,7 +598,8 @@
boolean ignoresSetting,
boolean requiresProx,
boolean immediatelyReRegister,
- @DevicePostureController.DevicePostureInt int posture
+ @DevicePostureController.DevicePostureInt int posture,
+ boolean requiresAod
) {
mSensors = sensors;
mSetting = setting;
@@ -580,6 +610,7 @@
mRequiresTouchscreen = requiresTouchscreen;
mIgnoresSetting = ignoresSetting;
mRequiresProx = requiresProx;
+ mRequiresAod = requiresAod;
mPosture = posture;
mImmediatelyReRegister = immediatelyReRegister;
}
@@ -663,13 +694,13 @@
}
protected boolean enabledBySetting() {
- if (!mConfig.enabled(UserHandle.USER_CURRENT)) {
+ if (!mConfig.enabled(mUserTracker.getUserId())) {
return false;
} else if (TextUtils.isEmpty(mSetting)) {
return true;
}
return mSecureSettings.getIntForUser(mSetting, mSettingDefault ? 1 : 0,
- UserHandle.USER_CURRENT) != 0;
+ mUserTracker.getUserId()) != 0;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index e6d9865..de0bdd3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -20,10 +20,10 @@
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.PowerManager;
-import android.os.UserHandle;
import android.text.TextUtils;
import com.android.systemui.doze.dagger.DozeScope;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import java.io.PrintWriter;
@@ -57,6 +57,7 @@
private final AmbientDisplayConfiguration mConfig;
private final DozeLog mDozeLog;
private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
+ private final UserTracker mUserTracker;
private boolean mIsCarModeEnabled = false;
@@ -65,11 +66,13 @@
DozeHost dozeHost,
AmbientDisplayConfiguration config,
DozeLog dozeLog,
- Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
+ Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ UserTracker userTracker) {
mDozeHost = dozeHost;
mConfig = config;
mDozeLog = dozeLog;
mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
+ mUserTracker = userTracker;
}
@Override
@@ -148,7 +151,7 @@
private void handleCarModeExited() {
mDozeLog.traceCarModeEnded();
- mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+ mMachine.requestState(mConfig.alwaysOnEnabled(mUserTracker.getUserId())
? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
}
@@ -166,7 +169,7 @@
if (mDozeHost.isPowerSaveActive()) {
nextState = DozeMachine.State.DOZE;
} else if (mMachine.getState() == DozeMachine.State.DOZE
- && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+ && mConfig.alwaysOnEnabled(mUserTracker.getUserId())) {
nextState = DozeMachine.State.DOZE_AOD;
}
@@ -181,7 +184,7 @@
// handles suppression changes, while DozeMachine#transitionPolicy handles gating
// transitions to DOZE_AOD
final DozeMachine.State nextState;
- if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) && !suppressed) {
+ if (mConfig.alwaysOnEnabled(mUserTracker.getUserId()) && !suppressed) {
nextState = DozeMachine.State.DOZE_AOD;
} else {
nextState = DozeMachine.State.DOZE;
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index b95c3f3..b709608 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -111,6 +111,7 @@
private boolean mWantProxSensor;
private boolean mWantTouchScreenSensors;
private boolean mWantSensors;
+ private boolean mInAod;
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -460,12 +461,19 @@
mDozeSensors.requestTemporaryDisable();
break;
case DOZE:
- case DOZE_AOD:
mAodInterruptRunnable = null;
- mWantProxSensor = newState != DozeMachine.State.DOZE;
+ mWantProxSensor = false;
mWantSensors = true;
mWantTouchScreenSensors = true;
- if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) {
+ mInAod = false;
+ break;
+ case DOZE_AOD:
+ mAodInterruptRunnable = null;
+ mWantProxSensor = true;
+ mWantSensors = true;
+ mWantTouchScreenSensors = true;
+ mInAod = true;
+ if (!sWakeDisplaySensorState) {
onWakeScreen(false, newState, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE);
}
break;
@@ -491,7 +499,7 @@
break;
default:
}
- mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors);
+ mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, mInAod);
}
private void registerCallbacks() {
@@ -510,11 +518,12 @@
private void stopListeningToAllTriggers() {
unregisterCallbacks();
- mDozeSensors.setListening(false, false);
+ mDozeSensors.setListening(false, false, false);
mDozeSensors.setProxListening(false);
mWantSensors = false;
mWantProxSensor = false;
mWantTouchScreenSensors = false;
+ mInAod = false;
}
@Override
@@ -523,7 +532,8 @@
final boolean lowPowerStateOrOff = state == Display.STATE_DOZE
|| state == Display.STATE_DOZE_SUSPEND || state == Display.STATE_OFF;
mDozeSensors.setProxListening(mWantProxSensor && lowPowerStateOrOff);
- mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, lowPowerStateOrOff);
+ mDozeSensors.setListeningWithPowerState(mWantSensors, mWantTouchScreenSensors,
+ mInAod, lowPowerStateOrOff);
if (mAodInterruptRunnable != null && state == Display.STATE_ON) {
mAodInterruptRunnable.run();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamMonitor.java
new file mode 100644
index 0000000..102f208
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamMonitor.java
@@ -0,0 +1,58 @@
+/*
+ * 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.dreams;
+
+import android.util.Log;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.dreams.callbacks.DreamStatusBarStateCallback;
+import com.android.systemui.dreams.conditions.DreamCondition;
+import com.android.systemui.shared.condition.Monitor;
+
+import javax.inject.Inject;
+
+/**
+ * A {@link CoreStartable} to retain a monitor for tracking dreaming.
+ */
+public class DreamMonitor implements CoreStartable {
+ private static final String TAG = "DreamMonitor";
+
+ // We retain a reference to the monitor so it is not garbage-collected.
+ private final Monitor mConditionMonitor;
+ private final DreamCondition mDreamCondition;
+ private final DreamStatusBarStateCallback mCallback;
+
+
+ @Inject
+ public DreamMonitor(Monitor monitor, DreamCondition dreamCondition,
+ DreamStatusBarStateCallback callback) {
+ mConditionMonitor = monitor;
+ mDreamCondition = dreamCondition;
+ mCallback = callback;
+
+ }
+ @Override
+ public void start() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "started");
+ }
+
+ mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(mCallback)
+ .addCondition(mDreamCondition)
+ .build());
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index c882f8a..c3bd5d9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -182,6 +182,18 @@
}
}
+ /**
+ * Ends the dream content and dream overlay animations, if they're currently running.
+ * @see [AnimatorSet.end]
+ */
+ fun endAnimations() {
+ mAnimator =
+ mAnimator?.let {
+ it.end()
+ null
+ }
+ }
+
private fun blurAnimator(
view: View,
fromBlurRadius: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 3106173..50cfb6a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -37,10 +37,11 @@
import com.android.systemui.dreams.complication.ComplicationHostViewController;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.dreams.dagger.DreamOverlayModule;
+import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.statusbar.BlurUtils;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -55,7 +56,6 @@
@DreamOverlayComponent.DreamOverlayScope
public class DreamOverlayContainerViewController extends ViewController<DreamOverlayContainerView> {
private final DreamOverlayStatusBarViewController mStatusBarViewController;
- private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private final BlurUtils mBlurUtils;
private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
private final DreamOverlayStateController mStateController;
@@ -84,9 +84,26 @@
private long mJitterStartTimeMillis;
private boolean mBouncerAnimating;
+ private boolean mWakingUpFromSwipe;
- private final KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback =
- new KeyguardBouncer.PrimaryBouncerExpansionCallback() {
+ private final BouncerlessScrimController mBouncerlessScrimController;
+
+ private final BouncerlessScrimController.Callback mBouncerlessExpansionCallback =
+ new BouncerlessScrimController.Callback() {
+ @Override
+ public void onExpansion(ShadeExpansionChangeEvent event) {
+ updateTransitionState(event.getFraction());
+ }
+
+ @Override
+ public void onWakeup() {
+ mWakingUpFromSwipe = true;
+ }
+ };
+
+ private final PrimaryBouncerExpansionCallback
+ mBouncerExpansionCallback =
+ new PrimaryBouncerExpansionCallback() {
@Override
public void onStartingToShow() {
@@ -125,13 +142,29 @@
}
};
+ /**
+ * If true, overlay entry animations should be skipped once.
+ *
+ * This is turned on when exiting low light and should be turned off once the entry animations
+ * are skipped once.
+ */
+ private boolean mSkipEntryAnimations;
+
+ private final DreamOverlayStateController.Callback
+ mDreamOverlayStateCallback =
+ new DreamOverlayStateController.Callback() {
+ @Override
+ public void onExitLowLight() {
+ mSkipEntryAnimations = true;
+ }
+ };
+
@Inject
public DreamOverlayContainerViewController(
DreamOverlayContainerView containerView,
ComplicationHostViewController complicationHostViewController,
@Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView,
DreamOverlayStatusBarViewController statusBarViewController,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager,
BlurUtils blurUtils,
@Main Handler handler,
@Main Resources resources,
@@ -141,15 +174,18 @@
@Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter,
PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
DreamOverlayAnimationsController animationsController,
- DreamOverlayStateController stateController) {
+ DreamOverlayStateController stateController,
+ BouncerlessScrimController bouncerlessScrimController) {
super(containerView);
mDreamOverlayContentView = contentView;
mStatusBarViewController = statusBarViewController;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mBlurUtils = blurUtils;
mDreamOverlayAnimationsController = animationsController;
mStateController = stateController;
+ mBouncerlessScrimController = bouncerlessScrimController;
+ mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
+
mComplicationHostViewController = complicationHostViewController;
mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
R.dimen.dream_overlay_y_offset);
@@ -168,6 +204,7 @@
@Override
protected void onInit() {
+ mStateController.addCallback(mDreamOverlayStateCallback);
mStatusBarViewController.init();
mComplicationHostViewController.init();
mDreamOverlayAnimationsController.init(mView);
@@ -175,27 +212,27 @@
@Override
protected void onViewAttached() {
+ mWakingUpFromSwipe = false;
mJitterStartTimeMillis = System.currentTimeMillis();
mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
- final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getPrimaryBouncer();
- if (bouncer != null) {
- bouncer.addBouncerExpansionCallback(mBouncerExpansionCallback);
- }
mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
// Start dream entry animations. Skip animations for low light clock.
if (!mStateController.isLowLightActive()) {
mDreamOverlayAnimationsController.startEntryAnimations();
+
+ if (mSkipEntryAnimations) {
+ // If we're transitioning from the low light dream back to the user dream, skip the
+ // overlay animations and show immediately.
+ mDreamOverlayAnimationsController.endAnimations();
+ mSkipEntryAnimations = false;
+ }
}
}
@Override
protected void onViewDetached() {
mHandler.removeCallbacks(this::updateBurnInOffsets);
- final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getPrimaryBouncer();
- if (bouncer != null) {
- bouncer.removeBouncerExpansionCallback(mBouncerExpansionCallback);
- }
mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
mDreamOverlayAnimationsController.cancelAnimations();
@@ -264,6 +301,13 @@
*/
public void wakeUp(@NonNull Runnable onAnimationEnd,
@NonNull DelayableExecutor callbackExecutor) {
+ // When swiping causes wakeup, do not run any animations as the dream should exit as soon
+ // as possible.
+ if (mWakingUpFromSwipe) {
+ onAnimationEnd.run();
+ return;
+ }
+
mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index a9a9cae..dd01be0 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -208,6 +208,13 @@
});
}
+ @Override
+ public void onEndDream() {
+ mExecutor.execute(() -> {
+ resetCurrentDreamOverlayLocked();
+ });
+ }
+
private Lifecycle.State getCurrentStateLocked() {
return mLifecycleRegistry.getCurrentState();
}
@@ -291,6 +298,7 @@
mDreamOverlayContainerViewController = null;
mDreamOverlayTouchMonitor = null;
+ mWindow = null;
mStarted = false;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index ccfdd096..2c7ecb1 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -83,6 +83,12 @@
*/
default void onAvailableComplicationTypesChanged() {
}
+
+ /**
+ * Called when the low light dream is exiting and transitioning back to the user dream.
+ */
+ default void onExitLowLight() {
+ }
}
private final Executor mExecutor;
@@ -278,6 +284,10 @@
* @param active {@code true} if low light mode is active, {@code false} otherwise.
*/
public void setLowLightActive(boolean active) {
+ if (isLowLightActive() && !active) {
+ // Notify that we're exiting low light only on the transition from active to not active.
+ mCallbacks.forEach(Callback::onExitLowLight);
+ }
modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_LOW_LIGHT_ACTIVE);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index f244cb0..96bce4c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@@ -26,6 +27,9 @@
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.systemui.R;
+import com.android.systemui.shared.shadow.DoubleShadowIconDrawable;
+import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo;
+import com.android.systemui.statusbar.AlphaOptimizedImageView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -60,8 +64,15 @@
public static final int STATUS_ICON_PRIORITY_MODE_ON = 6;
private final Map<Integer, View> mStatusIcons = new HashMap<>();
+ private Context mContext;
private ViewGroup mSystemStatusViewGroup;
private ViewGroup mExtraSystemStatusViewGroup;
+ private ShadowInfo mKeyShadowInfo;
+ private ShadowInfo mAmbientShadowInfo;
+ private int mDrawableSize;
+ private int mDrawableInsetSize;
+ private static final float KEY_SHADOW_ALPHA = 0.35f;
+ private static final float AMBIENT_SHADOW_ALPHA = 0.4f;
public DreamOverlayStatusBarView(Context context) {
this(context, null);
@@ -73,6 +84,7 @@
public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
+ mContext = context;
}
public DreamOverlayStatusBarView(
@@ -80,14 +92,36 @@
super(context, attrs, defStyleAttr, defStyleRes);
}
+
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ mKeyShadowInfo = createShadowInfo(
+ R.dimen.dream_overlay_status_bar_key_text_shadow_radius,
+ R.dimen.dream_overlay_status_bar_key_text_shadow_dx,
+ R.dimen.dream_overlay_status_bar_key_text_shadow_dy,
+ KEY_SHADOW_ALPHA
+ );
+
+ mAmbientShadowInfo = createShadowInfo(
+ R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius,
+ R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx,
+ R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy,
+ AMBIENT_SHADOW_ALPHA
+ );
+
+ mDrawableSize = mContext
+ .getResources()
+ .getDimensionPixelSize(R.dimen.dream_overlay_status_bar_icon_size);
+ mDrawableInsetSize = mContext
+ .getResources()
+ .getDimensionPixelSize(R.dimen.dream_overlay_icon_inset_dimen);
+
mStatusIcons.put(STATUS_ICON_WIFI_UNAVAILABLE,
- fetchStatusIconForResId(R.id.dream_overlay_wifi_status));
+ addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_wifi_status)));
mStatusIcons.put(STATUS_ICON_ALARM_SET,
- fetchStatusIconForResId(R.id.dream_overlay_alarm_set));
+ addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_alarm_set)));
mStatusIcons.put(STATUS_ICON_CAMERA_DISABLED,
fetchStatusIconForResId(R.id.dream_overlay_camera_off));
mStatusIcons.put(STATUS_ICON_MIC_DISABLED,
@@ -97,7 +131,7 @@
mStatusIcons.put(STATUS_ICON_NOTIFICATIONS,
fetchStatusIconForResId(R.id.dream_overlay_notification_indicator));
mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON,
- fetchStatusIconForResId(R.id.dream_overlay_priority_mode));
+ addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode)));
mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status);
mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items);
@@ -137,4 +171,34 @@
}
return false;
}
+
+ private View addDoubleShadow(View icon) {
+ if (icon instanceof AlphaOptimizedImageView) {
+ AlphaOptimizedImageView i = (AlphaOptimizedImageView) icon;
+ Drawable drawableIcon = i.getDrawable();
+ i.setImageDrawable(new DoubleShadowIconDrawable(
+ mKeyShadowInfo,
+ mAmbientShadowInfo,
+ drawableIcon,
+ mDrawableSize,
+ mDrawableInsetSize
+ ));
+ }
+ return icon;
+ }
+
+ private ShadowInfo createShadowInfo(int blurId, int offsetXId, int offsetYId, float alpha) {
+ return new ShadowInfo(
+ fetchDimensionForResId(blurId),
+ fetchDimensionForResId(offsetXId),
+ fetchDimensionForResId(offsetYId),
+ alpha
+ );
+ }
+
+ private Float fetchDimensionForResId(int resId) {
+ return mContext
+ .getResources()
+ .getDimension(resId);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index f1bb156..90c440c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -25,7 +25,6 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
-import android.os.UserHandle;
import android.provider.Settings;
import android.text.format.DateFormat;
import android.util.PluralsMessageFormatter;
@@ -37,6 +36,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CrossFadeHelper;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.statusbar.policy.NextAlarmController;
@@ -72,6 +72,7 @@
mDreamOverlayNotificationCountProvider;
private final ZenModeController mZenModeController;
private final DreamOverlayStateController mDreamOverlayStateController;
+ private final UserTracker mUserTracker;
private final StatusBarWindowStateController mStatusBarWindowStateController;
private final DreamOverlayStatusBarItemsProvider mStatusBarItemsProvider;
private final Executor mMainExecutor;
@@ -154,7 +155,8 @@
ZenModeController zenModeController,
StatusBarWindowStateController statusBarWindowStateController,
DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
- DreamOverlayStateController dreamOverlayStateController) {
+ DreamOverlayStateController dreamOverlayStateController,
+ UserTracker userTracker) {
super(view);
mResources = resources;
mMainExecutor = mainExecutor;
@@ -169,6 +171,7 @@
mStatusBarItemsProvider = statusBarItemsProvider;
mZenModeController = zenModeController;
mDreamOverlayStateController = dreamOverlayStateController;
+ mUserTracker = userTracker;
// Register to receive show/hide updates for the system status bar. Our custom status bar
// needs to hide when the system status bar is showing to ovoid overlapping status bars.
@@ -259,7 +262,7 @@
private void updateAlarmStatusIcon() {
final AlarmManager.AlarmClockInfo alarm =
- mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+ mAlarmManager.getNextAlarmClock(mUserTracker.getUserId());
final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
showIcon(
DreamOverlayStatusBarView.STATUS_ICON_ALARM_SET,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/callbacks/DreamStatusBarStateCallback.java b/packages/SystemUI/src/com/android/systemui/dreams/callbacks/DreamStatusBarStateCallback.java
new file mode 100644
index 0000000..c8c9470
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/callbacks/DreamStatusBarStateCallback.java
@@ -0,0 +1,46 @@
+/*
+ * 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.dreams.callbacks;
+
+import android.util.Log;
+
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+
+import javax.inject.Inject;
+
+/**
+ * A callback that informs {@link SysuiStatusBarStateController} when the dream state has changed.
+ */
+public class DreamStatusBarStateCallback implements Monitor.Callback {
+ private static final String TAG = "DreamStatusBarCallback";
+
+ private final SysuiStatusBarStateController mStateController;
+
+ @Inject
+ public DreamStatusBarStateCallback(SysuiStatusBarStateController statusBarStateController) {
+ mStateController = statusBarStateController;
+ }
+
+ @Override
+ public void onConditionsChanged(boolean allConditionsMet) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConditionChanged:" + allConditionsMet);
+ }
+
+ mStateController.setIsDreaming(allConditionsMet);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java
new file mode 100644
index 0000000..2befce7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java
@@ -0,0 +1,73 @@
+/*
+ * 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.dreams.conditions;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.text.TextUtils;
+
+import com.android.systemui.shared.condition.Condition;
+
+import javax.inject.Inject;
+
+/**
+ * {@link DreamCondition} provides a signal when a dream begins and ends.
+ */
+public class DreamCondition extends Condition {
+ private final Context mContext;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ processIntent(intent);
+ }
+ };
+
+ @Inject
+ public DreamCondition(Context context) {
+ mContext = context;
+ }
+
+ private void processIntent(Intent intent) {
+ // In the case of a non-existent sticky broadcast, ignore when there is no intent.
+ if (intent == null) {
+ return;
+ }
+ if (TextUtils.equals(intent.getAction(), Intent.ACTION_DREAMING_STARTED)) {
+ updateCondition(true);
+ } else if (TextUtils.equals(intent.getAction(), Intent.ACTION_DREAMING_STOPPED)) {
+ updateCondition(false);
+ } else {
+ throw new IllegalStateException("unexpected intent:" + intent);
+ }
+ }
+
+ @Override
+ protected void start() {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_DREAMING_STARTED);
+ filter.addAction(Intent.ACTION_DREAMING_STOPPED);
+ final Intent stickyIntent = mContext.registerReceiver(mReceiver, filter);
+ processIntent(stickyIntent);
+ }
+
+ @Override
+ protected void stop() {
+ mContext.unregisterReceiver(mReceiver);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index e7b29bb..0ab8c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -28,6 +28,7 @@
import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
import com.android.systemui.dreams.DreamOverlayService;
import com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule;
+import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule;
import java.util.Optional;
@@ -42,6 +43,7 @@
@Module(includes = {
RegisteredComplicationsModule.class,
LowLightDreamModule.class,
+ ScrimModule.class
},
subcomponents = {
DreamOverlayComponent.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
index 92cdcf9..73c2289 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -36,11 +36,12 @@
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.dreams.touch.scrim.ScrimController;
+import com.android.systemui.dreams.touch.scrim.ScrimManager;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.wm.shell.animation.FlingAnimationUtils;
import java.util.Optional;
@@ -78,7 +79,8 @@
private final NotificationShadeWindowController mNotificationShadeWindowController;
private final float mBouncerZoneScreenPercentage;
- private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private final ScrimManager mScrimManager;
+ private ScrimController mCurrentScrimController;
private float mCurrentExpansion;
private final Optional<CentralSurfaces> mCentralSurfaces;
@@ -90,6 +92,7 @@
private final DisplayMetrics mDisplayMetrics;
private Boolean mCapture;
+ private Boolean mExpanded;
private boolean mBouncerInitiallyShowing;
@@ -101,6 +104,17 @@
private final UiEventLogger mUiEventLogger;
+ private final ScrimManager.Callback mScrimManagerCallback = new ScrimManager.Callback() {
+ @Override
+ public void onScrimControllerChanged(ScrimController controller) {
+ if (mCurrentScrimController != null) {
+ mCurrentScrimController.reset();
+ }
+
+ mCurrentScrimController = controller;
+ }
+ };
+
private final GestureDetector.OnGestureListener mOnGestureListener =
new GestureDetector.SimpleOnGestureListener() {
@Override
@@ -115,8 +129,10 @@
.orElse(false);
if (mCapture) {
+ // reset expanding
+ mExpanded = false;
// Since the user is dragging the bouncer up, set scrimmed to false.
- mStatusBarKeyguardViewManager.showPrimaryBouncer(false);
+ mCurrentScrimController.show();
}
}
@@ -157,10 +173,10 @@
ShadeExpansionChangeEvent event =
new ShadeExpansionChangeEvent(
/* fraction= */ mCurrentExpansion,
- /* expanded= */ false,
+ /* expanded= */ mExpanded,
/* tracking= */ true,
/* dragDownPxAmount= */ dragDownAmount);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(event);
+ mCurrentScrimController.expand(event);
}
@@ -187,7 +203,7 @@
@Inject
public BouncerSwipeTouchHandler(
DisplayMetrics displayMetrics,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+ ScrimManager scrimManager,
Optional<CentralSurfaces> centralSurfaces,
NotificationShadeWindowController notificationShadeWindowController,
ValueAnimatorCreator valueAnimatorCreator,
@@ -200,7 +216,7 @@
UiEventLogger uiEventLogger) {
mDisplayMetrics = displayMetrics;
mCentralSurfaces = centralSurfaces;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ mScrimManager = scrimManager;
mNotificationShadeWindowController = notificationShadeWindowController;
mBouncerZoneScreenPercentage = swipeRegionPercentage;
mFlingAnimationUtils = flingAnimationUtils;
@@ -234,9 +250,12 @@
mTouchSession = session;
mVelocityTracker.clear();
mNotificationShadeWindowController.setForcePluginOpen(true, this);
+ mScrimManager.addCallback(mScrimManagerCallback);
+ mCurrentScrimController = mScrimManager.getCurrentController();
session.registerCallback(() -> {
mVelocityTracker.recycle();
+ mScrimManager.removeCallback(mScrimManagerCallback);
mCapture = null;
mNotificationShadeWindowController.setForcePluginOpen(false, this);
});
@@ -273,18 +292,21 @@
final float velocityVector =
(float) Math.hypot(horizontalVelocity, verticalVelocity);
- final float expansion = flingRevealsOverlay(verticalVelocity, velocityVector)
- ? KeyguardBouncer.EXPANSION_HIDDEN : KeyguardBouncer.EXPANSION_VISIBLE;
+ mExpanded = !flingRevealsOverlay(verticalVelocity, velocityVector);
+ final float expansion = mExpanded
+ ? KeyguardBouncerConstants.EXPANSION_VISIBLE
+ : KeyguardBouncerConstants.EXPANSION_HIDDEN;
// Log the swiping up to show Bouncer event.
- if (!mBouncerInitiallyShowing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
+ if (!mBouncerInitiallyShowing
+ && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
mUiEventLogger.log(DreamEvent.DREAM_SWIPED);
}
flingToExpansion(verticalVelocity, expansion);
- if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
- mStatusBarKeyguardViewManager.reset(false);
+ if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
+ mCurrentScrimController.reset();
}
break;
default:
@@ -302,7 +324,8 @@
float dragDownAmount = expansionFraction * expansionHeight;
setPanelExpansion(expansionFraction, dragDownAmount);
});
- if (!mBouncerInitiallyShowing && targetExpansion == KeyguardBouncer.EXPANSION_VISIBLE) {
+ if (!mBouncerInitiallyShowing
+ && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
animator.addListener(
new AnimatorListenerAdapter() {
@Override
@@ -335,7 +358,7 @@
final float targetHeight = viewHeight * expansion;
final float expansionHeight = targetHeight - currentHeight;
final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight);
- if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
+ if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
// Hides the bouncer, i.e., fully expands the space above the bouncer.
mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity,
viewHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
index 695b59a..b8b459e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
@@ -226,6 +226,15 @@
return;
}
+ // When we stop monitoring touches, we must ensure that all active touch sessions and
+ // descendants informed of the removal so any cleanup for active tracking can proceed.
+ mExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> {
+ while (touchSession != null) {
+ touchSession.onRemoved();
+ touchSession = touchSession.getPredecessor();
+ }
+ }));
+
mCurrentInputSession.dispose();
mCurrentInputSession = null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
index 4382757..e1d0339 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
@@ -21,10 +21,10 @@
import android.os.Looper;
import android.view.Choreographer;
-import android.view.Display;
import android.view.GestureDetector;
import android.view.MotionEvent;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -55,8 +55,9 @@
public InputSession(@Named(INPUT_SESSION_NAME) String sessionName,
InputChannelCompat.InputEventListener inputEventListener,
GestureDetector.OnGestureListener gestureListener,
+ DisplayTracker displayTracker,
@Named(PILFER_ON_GESTURE_CONSUME) boolean pilferOnGestureConsume) {
- mInputMonitor = new InputMonitorCompat(sessionName, Display.DEFAULT_DISPLAY);
+ mInputMonitor = new InputMonitorCompat(sessionName, displayTracker.getDefaultDisplayId());
mGestureDetector = new GestureDetector(gestureListener);
mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(),
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerScrimController.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerScrimController.java
new file mode 100644
index 0000000..f5bbba7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerScrimController.java
@@ -0,0 +1,49 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+
+import javax.inject.Inject;
+
+/**
+ * Implementation for handling swipe movements on the overlay when the keyguard is present.
+ */
+public class BouncerScrimController implements ScrimController {
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+
+ @Inject
+ BouncerScrimController(StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ }
+
+ @Override
+ public void show() {
+ mStatusBarKeyguardViewManager.showBouncer(false);
+ }
+
+ @Override
+ public void expand(ShadeExpansionChangeEvent event) {
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(event);
+ }
+
+ @Override
+ public void reset() {
+ mStatusBarKeyguardViewManager.reset(false);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimController.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimController.java
new file mode 100644
index 0000000..01e4d04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimController.java
@@ -0,0 +1,91 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import android.os.PowerManager;
+import android.os.SystemClock;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.unfold.util.CallbackController;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/**
+ * {@link BouncerlessScrimController} handles scrim progression when no keyguard is set. When
+ * fully expanded, the controller dismisses the dream.
+ */
+@SysUISingleton
+public class BouncerlessScrimController implements ScrimController,
+ CallbackController<BouncerlessScrimController.Callback> {
+ private static final String TAG = "BLScrimController";
+
+ /**
+ * {@link Callback} allows {@link BouncerlessScrimController} clients to be informed of
+ * expansion progression and wakeup
+ */
+ public interface Callback {
+ /**
+ * Invoked when there is a change to the scrim expansion.
+ */
+ void onExpansion(ShadeExpansionChangeEvent event);
+
+ /**
+ * Invoked after {@link BouncerlessScrimController} has started waking up the device.
+ */
+ void onWakeup();
+ }
+
+ private final Executor mExecutor;
+ private final PowerManager mPowerManager;
+
+ @Override
+ public void addCallback(Callback listener) {
+ mExecutor.execute(() -> mCallbacks.add(listener));
+ }
+
+ @Override
+ public void removeCallback(Callback listener) {
+ mExecutor.execute(() -> mCallbacks.remove(listener));
+ }
+
+ private final HashSet<Callback> mCallbacks;
+
+
+ @Inject
+ public BouncerlessScrimController(@Main Executor executor,
+ PowerManager powerManager) {
+ mExecutor = executor;
+ mPowerManager = powerManager;
+ mCallbacks = new HashSet<>();
+ }
+
+ @Override
+ public void expand(ShadeExpansionChangeEvent event) {
+ if (event.getExpanded()) {
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ "com.android.systemui:SwipeUp");
+ mExecutor.execute(() -> mCallbacks.forEach(callback -> callback.onWakeup()));
+ } else {
+ mExecutor.execute(() -> mCallbacks.forEach(callback -> callback.onExpansion(event)));
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimController.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimController.java
new file mode 100644
index 0000000..61629ef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimController.java
@@ -0,0 +1,44 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+
+/**
+ * {@link ScrimController} provides an interface for the different consumers of scrolling/expansion
+ * events over the dream.
+ */
+public interface ScrimController {
+ /**
+ * Called at the start of expansion before any expansion amount updates.
+ */
+ default void show() {
+ }
+
+ /**
+ * Called for every expansion update.
+ * @param event {@link ShadeExpansionChangeEvent} detailing the change.
+ */
+ default void expand(ShadeExpansionChangeEvent event) {
+ }
+
+ /**
+ * Called at the end of the movement.
+ */
+ default void reset() {
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimManager.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimManager.java
new file mode 100644
index 0000000..0d0dff6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/ScrimManager.java
@@ -0,0 +1,112 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import static com.android.systemui.dreams.touch.scrim.dagger.ScrimModule.BOUNCERLESS_SCRIM_CONTROLLER;
+import static com.android.systemui.dreams.touch.scrim.dagger.ScrimModule.BOUNCER_SCRIM_CONTROLLER;
+
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * {@link ScrimManager} helps manage multiple {@link ScrimController} instances, specifying the
+ * appropriate one to use at the current moment and managing the handoff between controllers.
+ */
+public class ScrimManager {
+ private final ScrimController mBouncerScrimController;
+ private final ScrimController mBouncerlessScrimController;
+ private final KeyguardStateController mKeyguardStateController;
+ private final Executor mExecutor;
+
+ private ScrimController mCurrentController;
+ private final HashSet<Callback> mCallbacks;
+
+ /**
+ * Interface implemented for receiving updates to the active {@link ScrimController}.
+ */
+ public interface Callback {
+ /**
+ * Invoked when the controller changes.
+ * @param controller The currently active {@link ScrimController}.
+ */
+ void onScrimControllerChanged(ScrimController controller);
+ }
+
+ private final KeyguardStateController.Callback mKeyguardStateCallback =
+ new KeyguardStateController.Callback() {
+ @Override
+ public void onKeyguardShowingChanged() {
+ mExecutor.execute(() -> updateController());
+ }
+ };
+
+ @Inject
+ ScrimManager(@Main Executor executor,
+ @Named(BOUNCER_SCRIM_CONTROLLER) ScrimController bouncerScrimController,
+ @Named(BOUNCERLESS_SCRIM_CONTROLLER)ScrimController bouncerlessScrimController,
+ KeyguardStateController keyguardStateController) {
+ mExecutor = executor;
+ mCallbacks = new HashSet<>();
+ mBouncerlessScrimController = bouncerlessScrimController;
+ mBouncerScrimController = bouncerScrimController;
+ mKeyguardStateController = keyguardStateController;
+
+ mKeyguardStateController.addCallback(mKeyguardStateCallback);
+ updateController();
+ }
+
+ private void updateController() {
+ final ScrimController existingController = mCurrentController;
+ mCurrentController = mKeyguardStateController.canDismissLockScreen()
+ ? mBouncerlessScrimController
+ : mBouncerScrimController;
+
+ if (existingController == mCurrentController) {
+ return;
+ }
+
+ mCallbacks.forEach(callback -> callback.onScrimControllerChanged(mCurrentController));
+ }
+
+ /**
+ * Adds a {@link Callback} to receive future changes to the active {@link ScrimController}.
+ */
+ public void addCallback(Callback callback) {
+ mExecutor.execute(() -> mCallbacks.add(callback));
+ }
+
+ /**
+ * Removes the {@link Callback} from receiving further updates.
+ */
+ public void removeCallback(Callback callback) {
+ mExecutor.execute(() -> mCallbacks.remove(callback));
+ }
+
+ /**
+ * Returns the currently get {@link ScrimController}.
+ * @return
+ */
+ public ScrimController getCurrentController() {
+ return mCurrentController;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/dagger/ScrimModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/dagger/ScrimModule.java
new file mode 100644
index 0000000..40bc0ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/scrim/dagger/ScrimModule.java
@@ -0,0 +1,51 @@
+/*
+ * 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.dreams.touch.scrim.dagger;
+
+import com.android.systemui.dreams.touch.scrim.BouncerScrimController;
+import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
+import com.android.systemui.dreams.touch.scrim.ScrimController;
+
+import javax.inject.Named;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Module for scrim related dependencies.
+ */
+@Module
+public interface ScrimModule {
+ String BOUNCERLESS_SCRIM_CONTROLLER = "bouncerless_scrim_controller";
+ String BOUNCER_SCRIM_CONTROLLER = "bouncer_scrim_controller";
+
+ /** */
+ @Provides
+ @Named(BOUNCERLESS_SCRIM_CONTROLLER)
+ static ScrimController providesBouncerlessScrimController(
+ BouncerlessScrimController controller) {
+ return controller;
+ }
+
+ /** */
+ @Provides
+ @Named(BOUNCER_SCRIM_CONTROLLER)
+ static ScrimController providesBouncerScrimController(
+ BouncerScrimController controller) {
+ return controller;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index d1a14a1..4bac697 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -19,7 +19,7 @@
import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS;
import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG;
import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS;
-import static com.android.systemui.flags.FlagManager.EXTRA_ID;
+import static com.android.systemui.flags.FlagManager.EXTRA_NAME;
import static com.android.systemui.flags.FlagManager.EXTRA_VALUE;
import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS;
@@ -39,6 +39,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings;
import org.jetbrains.annotations.NotNull;
@@ -72,14 +73,15 @@
private final FlagManager mFlagManager;
private final Context mContext;
+ private final GlobalSettings mGlobalSettings;
private final SecureSettings mSecureSettings;
private final Resources mResources;
private final SystemPropertiesHelper mSystemProperties;
private final ServerFlagReader mServerFlagReader;
- private final Map<Integer, Flag<?>> mAllFlags;
- private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>();
- private final Map<Integer, String> mStringFlagCache = new TreeMap<>();
- private final Map<Integer, Integer> mIntFlagCache = new TreeMap<>();
+ private final Map<String, Flag<?>> mAllFlags;
+ private final Map<String, Boolean> mBooleanFlagCache = new TreeMap<>();
+ private final Map<String, String> mStringFlagCache = new TreeMap<>();
+ private final Map<String, Integer> mIntFlagCache = new TreeMap<>();
private final Restarter mRestarter;
private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
@@ -94,14 +96,16 @@
public FeatureFlagsDebug(
FlagManager flagManager,
Context context,
+ GlobalSettings globalSettings,
SecureSettings secureSettings,
SystemPropertiesHelper systemProperties,
@Main Resources resources,
ServerFlagReader serverFlagReader,
- @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
+ @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags,
Restarter restarter) {
mFlagManager = flagManager;
mContext = context;
+ mGlobalSettings = globalSettings;
mSecureSettings = secureSettings;
mResources = resources;
mSystemProperties = systemProperties;
@@ -133,96 +137,103 @@
}
private boolean isEnabledInternal(@NotNull BooleanFlag flag) {
- int id = flag.getId();
- if (!mBooleanFlagCache.containsKey(id)) {
- mBooleanFlagCache.put(id,
+ String name = flag.getName();
+ if (!mBooleanFlagCache.containsKey(name)) {
+ mBooleanFlagCache.put(name,
readBooleanFlagInternal(flag, flag.getDefault()));
}
- return mBooleanFlagCache.get(id);
+ return mBooleanFlagCache.get(name);
}
@Override
public boolean isEnabled(@NonNull ResourceBooleanFlag flag) {
- int id = flag.getId();
- if (!mBooleanFlagCache.containsKey(id)) {
- mBooleanFlagCache.put(id,
+ String name = flag.getName();
+ if (!mBooleanFlagCache.containsKey(name)) {
+ mBooleanFlagCache.put(name,
readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId())));
}
- return mBooleanFlagCache.get(id);
+ return mBooleanFlagCache.get(name);
}
@Override
public boolean isEnabled(@NonNull SysPropBooleanFlag flag) {
- int id = flag.getId();
- if (!mBooleanFlagCache.containsKey(id)) {
+ String name = flag.getName();
+ if (!mBooleanFlagCache.containsKey(name)) {
// Use #readFlagValue to get the default. That will allow it to fall through to
// teamfood if need be.
mBooleanFlagCache.put(
- id,
+ name,
mSystemProperties.getBoolean(
flag.getName(),
readBooleanFlagInternal(flag, flag.getDefault())));
}
- return mBooleanFlagCache.get(id);
+ return mBooleanFlagCache.get(name);
}
@NonNull
@Override
public String getString(@NonNull StringFlag flag) {
- int id = flag.getId();
- if (!mStringFlagCache.containsKey(id)) {
- mStringFlagCache.put(id,
- readFlagValueInternal(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
+ String name = flag.getName();
+ if (!mStringFlagCache.containsKey(name)) {
+ mStringFlagCache.put(name,
+ readFlagValueInternal(
+ flag.getId(), name, flag.getDefault(), StringFlagSerializer.INSTANCE));
}
- return mStringFlagCache.get(id);
+ return mStringFlagCache.get(name);
}
@NonNull
@Override
public String getString(@NonNull ResourceStringFlag flag) {
- int id = flag.getId();
- if (!mStringFlagCache.containsKey(id)) {
- mStringFlagCache.put(id,
- readFlagValueInternal(id, mResources.getString(flag.getResourceId()),
+ String name = flag.getName();
+ if (!mStringFlagCache.containsKey(name)) {
+ mStringFlagCache.put(name,
+ readFlagValueInternal(
+ flag.getId(), name, mResources.getString(flag.getResourceId()),
StringFlagSerializer.INSTANCE));
}
- return mStringFlagCache.get(id);
+ return mStringFlagCache.get(name);
}
@NonNull
@Override
public int getInt(@NonNull IntFlag flag) {
- int id = flag.getId();
- if (!mIntFlagCache.containsKey(id)) {
- mIntFlagCache.put(id,
- readFlagValueInternal(id, flag.getDefault(), IntFlagSerializer.INSTANCE));
+ String name = flag.getName();
+ if (!mIntFlagCache.containsKey(name)) {
+ mIntFlagCache.put(name,
+ readFlagValueInternal(
+ flag.getId(), name, flag.getDefault(), IntFlagSerializer.INSTANCE));
}
- return mIntFlagCache.get(id);
+ return mIntFlagCache.get(name);
}
@NonNull
@Override
public int getInt(@NonNull ResourceIntFlag flag) {
- int id = flag.getId();
- if (!mIntFlagCache.containsKey(id)) {
- mIntFlagCache.put(id,
- readFlagValueInternal(id, mResources.getInteger(flag.getResourceId()),
+ String name = flag.getName();
+ if (!mIntFlagCache.containsKey(name)) {
+ mIntFlagCache.put(name,
+ readFlagValueInternal(
+ flag.getId(), name, mResources.getInteger(flag.getResourceId()),
IntFlagSerializer.INSTANCE));
}
- return mIntFlagCache.get(id);
+ return mIntFlagCache.get(name);
}
/** Specific override for Boolean flags that checks against the teamfood list.*/
private boolean readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue) {
- Boolean result = readBooleanFlagOverride(flag.getId());
+ Boolean result = readBooleanFlagOverride(flag.getName());
+ if (result == null) {
+ result = readBooleanFlagOverride(flag.getId());
+ }
boolean hasServerOverride = mServerFlagReader.hasOverride(
flag.getNamespace(), flag.getName());
@@ -231,7 +242,7 @@
if (!hasServerOverride
&& !defaultValue
&& result == null
- && flag.getId() != Flags.TEAMFOOD.getId()
+ && !flag.getName().equals(Flags.TEAMFOOD.getName())
&& flag.getTeamfood()) {
return isEnabled(Flags.TEAMFOOD);
}
@@ -244,16 +255,31 @@
return readFlagValueInternal(id, BooleanFlagSerializer.INSTANCE);
}
+ private Boolean readBooleanFlagOverride(String name) {
+ return readFlagValueInternal(name, BooleanFlagSerializer.INSTANCE);
+ }
+
+ // TODO(b/265188950): Remove id from this method once ids are fully deprecated.
@NonNull
private <T> T readFlagValueInternal(
- int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
+ int id, String name, @NonNull T defaultValue, FlagSerializer<T> serializer) {
requireNonNull(defaultValue, "defaultValue");
- T result = readFlagValueInternal(id, serializer);
- return result == null ? defaultValue : result;
+ T resultForName = readFlagValueInternal(name, serializer);
+ if (resultForName == null) {
+ T resultForId = readFlagValueInternal(id, serializer);
+ if (resultForId == null) {
+ return defaultValue;
+ } else {
+ setFlagValue(name, resultForId, serializer);
+ return resultForId;
+ }
+ }
+ return resultForName;
}
/** Returns the stored value or null if not set. */
+ // TODO(b/265188950): Remove method this once ids are fully deprecated.
@Nullable
private <T> T readFlagValueInternal(int id, FlagSerializer<T> serializer) {
try {
@@ -264,51 +290,71 @@
return null;
}
- private <T> void setFlagValue(int id, @NonNull T value, FlagSerializer<T> serializer) {
+ /** Returns the stored value or null if not set. */
+ @Nullable
+ private <T> T readFlagValueInternal(String name, FlagSerializer<T> serializer) {
+ try {
+ return mFlagManager.readFlagValue(name, serializer);
+ } catch (Exception e) {
+ eraseInternal(name);
+ }
+ return null;
+ }
+
+ private <T> void setFlagValue(String name, @NonNull T value, FlagSerializer<T> serializer) {
requireNonNull(value, "Cannot set a null value");
- T currentValue = readFlagValueInternal(id, serializer);
+ T currentValue = readFlagValueInternal(name, serializer);
if (Objects.equals(currentValue, value)) {
- Log.i(TAG, "Flag id " + id + " is already " + value);
+ Log.i(TAG, "Flag id " + name + " is already " + value);
return;
}
final String data = serializer.toSettingsData(value);
if (data == null) {
- Log.w(TAG, "Failed to set id " + id + " to " + value);
+ Log.w(TAG, "Failed to set id " + name + " to " + value);
return;
}
- mSecureSettings.putStringForUser(mFlagManager.idToSettingsKey(id), data,
+ mGlobalSettings.putStringForUser(mFlagManager.nameToSettingsKey(name), data,
UserHandle.USER_CURRENT);
- Log.i(TAG, "Set id " + id + " to " + value);
- removeFromCache(id);
- mFlagManager.dispatchListenersAndMaybeRestart(id, this::restartSystemUI);
+ Log.i(TAG, "Set id " + name + " to " + value);
+ removeFromCache(name);
+ mFlagManager.dispatchListenersAndMaybeRestart(name, this::restartSystemUI);
}
<T> void eraseFlag(Flag<T> flag) {
if (flag instanceof SysPropFlag) {
mSystemProperties.erase(((SysPropFlag<T>) flag).getName());
- dispatchListenersAndMaybeRestart(flag.getId(), this::restartAndroid);
+ dispatchListenersAndMaybeRestart(flag.getName(), this::restartAndroid);
} else {
- eraseFlag(flag.getId());
+ eraseFlag(flag.getName());
}
}
/** Erase a flag's overridden value if there is one. */
- private void eraseFlag(int id) {
- eraseInternal(id);
- removeFromCache(id);
- dispatchListenersAndMaybeRestart(id, this::restartSystemUI);
+ private void eraseFlag(String name) {
+ eraseInternal(name);
+ removeFromCache(name);
+ dispatchListenersAndMaybeRestart(name, this::restartSystemUI);
}
- private void dispatchListenersAndMaybeRestart(int id, Consumer<Boolean> restartAction) {
- mFlagManager.dispatchListenersAndMaybeRestart(id, restartAction);
+ private void dispatchListenersAndMaybeRestart(String name, Consumer<Boolean> restartAction) {
+ mFlagManager.dispatchListenersAndMaybeRestart(name, restartAction);
}
- /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
+ /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */
+ // TODO(b/265188950): Remove method this once ids are fully deprecated.
private void eraseInternal(int id) {
- // We can't actually "erase" things from sysprops, but we can set them to empty!
- mSecureSettings.putStringForUser(mFlagManager.idToSettingsKey(id), "",
+ // We can't actually "erase" things from settings, but we can set them to empty!
+ mGlobalSettings.putStringForUser(mFlagManager.idToSettingsKey(id), "",
UserHandle.USER_CURRENT);
- Log.i(TAG, "Erase id " + id);
+ Log.i(TAG, "Erase name " + id);
+ }
+
+ /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */
+ private void eraseInternal(String name) {
+ // We can't actually "erase" things from settings, but we can set them to empty!
+ mGlobalSettings.putStringForUser(mFlagManager.nameToSettingsKey(name), "",
+ UserHandle.USER_CURRENT);
+ Log.i(TAG, "Erase name " + name);
}
@Override
@@ -339,13 +385,13 @@
void setBooleanFlagInternal(Flag<?> flag, boolean value) {
if (flag instanceof BooleanFlag) {
- setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
+ setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE);
} else if (flag instanceof ResourceBooleanFlag) {
- setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
+ setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE);
} else if (flag instanceof SysPropBooleanFlag) {
// Store SysProp flags in SystemProperties where they can read by outside parties.
mSystemProperties.setBoolean(((SysPropBooleanFlag) flag).getName(), value);
- dispatchListenersAndMaybeRestart(flag.getId(),
+ dispatchListenersAndMaybeRestart(flag.getName(),
FeatureFlagsDebug.this::restartAndroid);
} else {
throw new IllegalArgumentException("Unknown flag type");
@@ -354,9 +400,9 @@
void setStringFlagInternal(Flag<?> flag, String value) {
if (flag instanceof StringFlag) {
- setFlagValue(flag.getId(), value, StringFlagSerializer.INSTANCE);
+ setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE);
} else if (flag instanceof ResourceStringFlag) {
- setFlagValue(flag.getId(), value, StringFlagSerializer.INSTANCE);
+ setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE);
} else {
throw new IllegalArgumentException("Unknown flag type");
}
@@ -364,9 +410,9 @@
void setIntFlagInternal(Flag<?> flag, int value) {
if (flag instanceof IntFlag) {
- setFlagValue(flag.getId(), value, IntFlagSerializer.INSTANCE);
+ setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE);
} else if (flag instanceof ResourceIntFlag) {
- setFlagValue(flag.getId(), value, IntFlagSerializer.INSTANCE);
+ setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE);
} else {
throw new IllegalArgumentException("Unknown flag type");
}
@@ -405,17 +451,17 @@
Log.w(TAG, "No extras");
return;
}
- int id = extras.getInt(EXTRA_ID);
- if (id <= 0) {
- Log.w(TAG, "ID not set or less than or equal to 0: " + id);
+ String name = extras.getString(EXTRA_NAME);
+ if (name == null || name.isEmpty()) {
+ Log.w(TAG, "NAME not set or is empty: " + name);
return;
}
- if (!mAllFlags.containsKey(id)) {
- Log.w(TAG, "Tried to set unknown id: " + id);
+ if (!mAllFlags.containsKey(name)) {
+ Log.w(TAG, "Tried to set unknown name: " + name);
return;
}
- Flag<?> flag = mAllFlags.get(id);
+ Flag<?> flag = mAllFlags.get(name);
if (!extras.containsKey(EXTRA_VALUE)) {
eraseFlag(flag);
@@ -452,13 +498,16 @@
if (f instanceof ReleasedFlag) {
enabled = isEnabled((ReleasedFlag) f);
- overridden = readBooleanFlagOverride(f.getId()) != null;
+ overridden = readBooleanFlagOverride(f.getName()) != null
+ || readBooleanFlagOverride(f.getId()) != null;
} else if (f instanceof UnreleasedFlag) {
enabled = isEnabled((UnreleasedFlag) f);
- overridden = readBooleanFlagOverride(f.getId()) != null;
+ overridden = readBooleanFlagOverride(f.getName()) != null
+ || readBooleanFlagOverride(f.getId()) != null;
} else if (f instanceof ResourceBooleanFlag) {
enabled = isEnabled((ResourceBooleanFlag) f);
- overridden = readBooleanFlagOverride(f.getId()) != null;
+ overridden = readBooleanFlagOverride(f.getName()) != null
+ || readBooleanFlagOverride(f.getId()) != null;
} else if (f instanceof SysPropBooleanFlag) {
// TODO(b/223379190): Teamfood not supported for sysprop flags yet.
enabled = isEnabled((SysPropBooleanFlag) f);
@@ -480,9 +529,9 @@
}
};
- private void removeFromCache(int id) {
- mBooleanFlagCache.remove(id);
- mStringFlagCache.remove(id);
+ private void removeFromCache(String name) {
+ mBooleanFlagCache.remove(name);
+ mStringFlagCache.remove(name);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index b94d781..dc7fc28 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -21,6 +21,7 @@
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.util.InitializationChecker
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
@@ -34,7 +35,8 @@
private val commandRegistry: CommandRegistry,
private val flagCommand: FlagCommand,
private val featureFlags: FeatureFlagsDebug,
- private val broadcastSender: BroadcastSender
+ private val broadcastSender: BroadcastSender,
+ private val initializationChecker: InitializationChecker
) : CoreStartable {
init {
@@ -46,8 +48,11 @@
override fun start() {
featureFlags.init()
commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand }
- val intent = Intent(FlagManager.ACTION_SYSUI_STARTED)
- broadcastSender.sendBroadcast(intent)
+ if (initializationChecker.initializeComponents()) {
+ // protected broadcast should only be sent for the main process
+ val intent = Intent(FlagManager.ACTION_SYSUI_STARTED)
+ broadcastSender.sendBroadcast(intent)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index 8bddacc..7e14237 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -21,18 +21,16 @@
import static java.util.Objects.requireNonNull;
import android.content.res.Resources;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
import androidx.annotation.NonNull;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.DeviceConfigProxy;
import org.jetbrains.annotations.NotNull;
import java.io.PrintWriter;
+import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
@@ -50,12 +48,11 @@
private final Resources mResources;
private final SystemPropertiesHelper mSystemProperties;
- private final DeviceConfigProxy mDeviceConfigProxy;
private final ServerFlagReader mServerFlagReader;
private final Restarter mRestarter;
- private final Map<Integer, Flag<?>> mAllFlags;
- SparseBooleanArray mBooleanCache = new SparseBooleanArray();
- SparseArray<String> mStringCache = new SparseArray<>();
+ private final Map<String, Flag<?>> mAllFlags;
+ private final Map<String, Boolean> mBooleanCache = new HashMap<>();
+ private final Map<String, String> mStringCache = new HashMap<>();
private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
new ServerFlagReader.ChangeListener() {
@@ -69,13 +66,11 @@
public FeatureFlagsRelease(
@Main Resources resources,
SystemPropertiesHelper systemProperties,
- DeviceConfigProxy deviceConfigProxy,
ServerFlagReader serverFlagReader,
- @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
+ @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags,
Restarter restarter) {
mResources = resources;
mSystemProperties = systemProperties;
- mDeviceConfigProxy = deviceConfigProxy;
mServerFlagReader = serverFlagReader;
mAllFlags = allFlags;
mRestarter = restarter;
@@ -106,50 +101,48 @@
@Override
public boolean isEnabled(ResourceBooleanFlag flag) {
- int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
- if (cacheIndex < 0) {
- return isEnabled(flag.getId(), mResources.getBoolean(flag.getResourceId()));
+ if (!mBooleanCache.containsKey(flag.getName())) {
+ return isEnabled(flag.getName(), mResources.getBoolean(flag.getResourceId()));
}
- return mBooleanCache.valueAt(cacheIndex);
+ return mBooleanCache.get(flag.getName());
}
@Override
public boolean isEnabled(SysPropBooleanFlag flag) {
- int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
- if (cacheIndex < 0) {
+ if (!mBooleanCache.containsKey(flag.getName())) {
return isEnabled(
- flag.getId(), mSystemProperties.getBoolean(flag.getName(), flag.getDefault()));
+ flag.getName(),
+ mSystemProperties.getBoolean(flag.getName(), flag.getDefault()));
}
- return mBooleanCache.valueAt(cacheIndex);
+ return mBooleanCache.get(flag.getName());
}
- private boolean isEnabled(int key, boolean defaultValue) {
- mBooleanCache.append(key, defaultValue);
+ private boolean isEnabled(String name, boolean defaultValue) {
+ mBooleanCache.put(name, defaultValue);
return defaultValue;
}
@NonNull
@Override
public String getString(@NonNull StringFlag flag) {
- return getString(flag.getId(), flag.getDefault());
+ return getString(flag.getName(), flag.getDefault());
}
@NonNull
@Override
public String getString(@NonNull ResourceStringFlag flag) {
- int cacheIndex = mStringCache.indexOfKey(flag.getId());
- if (cacheIndex < 0) {
- return getString(flag.getId(),
+ if (!mStringCache.containsKey(flag.getName())) {
+ return getString(flag.getName(),
requireNonNull(mResources.getString(flag.getResourceId())));
}
- return mStringCache.valueAt(cacheIndex);
+ return mStringCache.get(flag.getName());
}
- private String getString(int key, String defaultValue) {
- mStringCache.append(key, defaultValue);
+ private String getString(String name, String defaultValue) {
+ mStringCache.put(name, defaultValue);
return defaultValue;
}
@@ -169,11 +162,17 @@
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("can override: false");
Map<String, Flag<?>> knownFlags = FlagsFactory.INSTANCE.getKnownFlags();
+ pw.println("Booleans: ");
for (Map.Entry<String, Flag<?>> nameToFlag : knownFlags.entrySet()) {
Flag<?> flag = nameToFlag.getValue();
- int id = flag.getId();
+ if (!(flag instanceof BooleanFlag)
+ || !(flag instanceof ResourceBooleanFlag)
+ || !(flag instanceof SysPropBooleanFlag)) {
+ continue;
+ }
+
boolean def = false;
- if (mBooleanCache.indexOfKey(flag.getId()) < 0) {
+ if (!mBooleanCache.containsKey(flag.getName())) {
if (flag instanceof SysPropBooleanFlag) {
SysPropBooleanFlag f = (SysPropBooleanFlag) flag;
def = mSystemProperties.getBoolean(f.getName(), f.getDefault());
@@ -185,15 +184,32 @@
def = f.getDefault();
}
}
- pw.println(" sysui_flag_" + id + ": " + (mBooleanCache.get(id, def)));
+ pw.println(
+ " " + flag.getName() + ": "
+ + (mBooleanCache.getOrDefault(flag.getName(), def)));
}
- int numStrings = mStringCache.size();
- pw.println("Strings: " + numStrings);
- for (int i = 0; i < numStrings; i++) {
- final int id = mStringCache.keyAt(i);
- final String value = mStringCache.valueAt(i);
- final int length = value.length();
- pw.println(" sysui_flag_" + id + ": [length=" + length + "] \"" + value + "\"");
+
+ pw.println("Strings: ");
+ for (Map.Entry<String, Flag<?>> nameToFlag : knownFlags.entrySet()) {
+ Flag<?> flag = nameToFlag.getValue();
+ if (!(flag instanceof StringFlag)
+ || !(flag instanceof ResourceStringFlag)) {
+ continue;
+ }
+
+ String def = "";
+ if (!mBooleanCache.containsKey(flag.getName())) {
+ if (flag instanceof ResourceStringFlag) {
+ ResourceStringFlag f = (ResourceStringFlag) flag;
+ def = mResources.getString(f.getResourceId());
+ } else if (flag instanceof StringFlag) {
+ StringFlag f = (StringFlag) flag;
+ def = f.getDefault();
+ }
+ }
+ String value = mStringCache.getOrDefault(flag.getName(), def);
+ pw.println(
+ " " + flag.getName() + ": [length=" + value.length() + "] \"" + value + "\"");
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
index b7fc0e4..daf9429 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
@@ -39,12 +39,12 @@
private final List<String> mOffCommands = List.of("false", "off", "0", "disable");
private final List<String> mSetCommands = List.of("set", "put");
private final FeatureFlagsDebug mFeatureFlags;
- private final Map<Integer, Flag<?>> mAllFlags;
+ private final Map<String, Flag<?>> mAllFlags;
@Inject
FlagCommand(
FeatureFlagsDebug featureFlags,
- @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags
+ @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags
) {
mFeatureFlags = featureFlags;
mAllFlags = allFlags;
@@ -53,30 +53,22 @@
@Override
public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) {
if (args.size() == 0) {
- pw.println("Error: no flag id supplied");
+ pw.println("Error: no flag name supplied");
help(pw);
pw.println();
printKnownFlags(pw);
return;
}
- int id = 0;
- try {
- id = Integer.parseInt(args.get(0));
- if (!mAllFlags.containsKey(id)) {
- pw.println("Unknown flag id: " + id);
- pw.println();
- printKnownFlags(pw);
- return;
- }
- } catch (NumberFormatException e) {
- id = flagNameToId(args.get(0));
- if (id == 0) {
- pw.println("Invalid flag. Must an integer id or flag name: " + args.get(0));
- return;
- }
+ String name = args.get(0);
+ if (!mAllFlags.containsKey(name)) {
+ pw.println("Unknown flag name: " + name);
+ pw.println();
+ printKnownFlags(pw);
+ return;
}
- Flag<?> flag = mAllFlags.get(id);
+
+ Flag<?> flag = mAllFlags.get(name);
String cmd = "";
if (args.size() > 1) {
@@ -117,7 +109,7 @@
return;
}
- pw.println("Flag " + id + " is " + newValue);
+ pw.println("Flag " + name + " is " + newValue);
pw.flush(); // Next command will restart sysui, so flush before we do so.
if (shouldSet) {
mFeatureFlags.setBooleanFlagInternal(flag, newValue);
@@ -136,11 +128,11 @@
return;
}
String value = args.get(2);
- pw.println("Setting Flag " + id + " to " + value);
+ pw.println("Setting Flag " + name + " to " + value);
pw.flush(); // Next command will restart sysui, so flush before we do so.
mFeatureFlags.setStringFlagInternal(flag, args.get(2));
} else {
- pw.println("Flag " + id + " is " + getStringFlag(flag));
+ pw.println("Flag " + name + " is " + getStringFlag(flag));
}
return;
} else if (isIntFlag(flag)) {
@@ -155,11 +147,11 @@
return;
}
int value = Integer.parseInt(args.get(2));
- pw.println("Setting Flag " + id + " to " + value);
+ pw.println("Setting Flag " + name + " to " + value);
pw.flush(); // Next command will restart sysui, so flush before we do so.
mFeatureFlags.setIntFlagInternal(flag, value);
} else {
- pw.println("Flag " + id + " is " + getIntFlag(flag));
+ pw.println("Flag " + name + " is " + getIntFlag(flag));
}
return;
}
@@ -182,8 +174,7 @@
private boolean isBooleanFlag(Flag<?> flag) {
return (flag instanceof BooleanFlag)
|| (flag instanceof ResourceBooleanFlag)
- || (flag instanceof SysPropFlag)
- || (flag instanceof DeviceConfigBooleanFlag);
+ || (flag instanceof SysPropFlag);
}
private boolean isBooleanFlagEnabled(Flag<?> flag) {
@@ -252,15 +243,14 @@
for (int i = 0; i < longestFieldName - "Flag Name".length() + 1; i++) {
pw.print(" ");
}
- pw.println("ID Value");
+ pw.println(" Value");
for (int i = 0; i < longestFieldName; i++) {
pw.print("=");
}
- pw.println(" ==== ========");
+ pw.println(" ========");
for (String fieldName : fields.keySet()) {
Flag<?> flag = fields.get(fieldName);
- int id = flag.getId();
- if (id == 0 || !mAllFlags.containsKey(id)) {
+ if (!mAllFlags.containsKey(flag.getName())) {
continue;
}
pw.print(fieldName);
@@ -268,9 +258,9 @@
for (int i = 0; i < longestFieldName - fieldWidth + 1; i++) {
pw.print(" ");
}
- pw.printf("%-4d ", id);
+ pw.print(" ");
if (isBooleanFlag(flag)) {
- pw.println(isBooleanFlagEnabled(mAllFlags.get(id)));
+ pw.println(isBooleanFlagEnabled(flag));
} else if (isStringFlag(flag)) {
pw.println(getStringFlag(flag));
} else if (isIntFlag(flag)) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 20ae64c..9595bc4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -64,6 +64,9 @@
// TODO(b/259130119): Tracking Bug
val FSI_ON_DND_UPDATE = unreleasedFlag(259130119, "fsi_on_dnd_update", teamfood = true)
+ // TODO(b/265804648): Tracking Bug
+ @JvmField val DISABLE_FSI = unreleasedFlag(265804648, "disable_fsi")
+
// TODO(b/254512538): Tracking Bug
val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true)
@@ -75,15 +78,7 @@
unreleasedFlag(119, "notification_memory_logging_enabled", teamfood = true)
// TODO(b/254512731): Tracking Bug
- @JvmField
- val NOTIFICATION_DISMISSAL_FADE =
- unreleasedFlag(113, "notification_dismissal_fade", teamfood = true)
-
- // TODO(b/259558771): Tracking Bug
- val STABILITY_INDEX_FIX = releasedFlag(114, "stability_index_fix")
-
- // TODO(b/259559750): Tracking Bug
- val SEMI_STABLE_SORT = releasedFlag(115, "semi_stable_sort")
+ @JvmField val NOTIFICATION_DISMISSAL_FADE = releasedFlag(113, "notification_dismissal_fade")
@JvmField val USE_ROUNDNESS_SOURCETYPES = releasedFlag(116, "use_roundness_sourcetype")
@@ -108,14 +103,16 @@
unreleasedFlag(174148361, "notification_inline_reply_animation", teamfood = true)
val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
- unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
+ releasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
+
+ // TODO(b/263414400): Tracking Bug
+ @JvmField
+ val NOTIFICATION_ANIMATE_BIG_PICTURE = unreleasedFlag(120, "notification_animate_big_picture")
// 200 - keyguard/lockscreen
// ** Flag retired **
// public static final BooleanFlag KEYGUARD_LAYOUT =
// new BooleanFlag(200, true);
- // TODO(b/254512713): Tracking Bug
- @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations")
// TODO(b/254512750): Tracking Bug
val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation")
@@ -131,13 +128,6 @@
val LOCKSCREEN_CUSTOM_CLOCKS = unreleasedFlag(207, "lockscreen_custom_clocks", teamfood = true)
/**
- * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual
- * replacement of KeyguardBouncer.java.
- */
- // TODO(b/254512385): Tracking Bug
- @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer")
-
- /**
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
* the digits when the clock moves.
*/
@@ -165,7 +155,7 @@
// TODO(b/255618149): Tracking Bug
@JvmField
val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES =
- unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false)
+ unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = true)
/** Shows chipbar UI whenever the device is unlocked by ActiveUnlock (watch). */
// TODO(b/256513609): Tracking Bug
@@ -180,6 +170,13 @@
@JvmField
val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
+ /**
+ * Whether to use the new alternate bouncer architecture, a refactor of and eventual replacement
+ * of the Alternate/Authentication Bouncer. No visual UI changes.
+ */
+ // TODO(b/260619425): Tracking Bug
+ @JvmField val MODERN_ALTERNATE_BOUNCER = releasedFlag(219, "modern_alternate_bouncer")
+
/** Flag to control the migration of face auth to modern architecture. */
// TODO(b/262838215): Tracking bug
@JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -194,14 +191,36 @@
/** A different path for unocclusion transitions back to keyguard */
// TODO(b/262859270): Tracking Bug
- @JvmField
- val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+ @JvmField val UNOCCLUSION_TRANSITION = releasedFlag(223, "unocclusion_transition")
// flag for controlling auto pin confirmation and material u shapes in bouncer
@JvmField
val AUTO_PIN_CONFIRMATION =
unreleasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
+ // TODO(b/262859270): Tracking Bug
+ @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
+
+ /** Enables code to show contextual loyalty cards in wallet entrypoints */
+ // TODO(b/247587924): Tracking Bug
+ @JvmField
+ val ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS =
+ unreleasedFlag(226, "enable_wallet_contextual_loyalty_cards", teamfood = false)
+
+ // TODO(b/242908637): Tracking Bug
+ @JvmField
+ val WALLPAPER_FULLSCREEN_PREVIEW =
+ unreleasedFlag(227, "wallpaper_fullscreen_preview", teamfood = true)
+
+ /** Whether the long-press gesture to open wallpaper picker is enabled. */
+ // TODO(b/266242192): Tracking Bug
+ @JvmField
+ val LOCK_SCREEN_LONG_PRESS_ENABLED =
+ unreleasedFlag(
+ 228,
+ "lock_screen_long_press_enabled",
+ )
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -211,11 +230,11 @@
// TODO(b/254513100): Tracking Bug
val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
releasedFlag(401, "smartspace_shared_element_transition_enabled")
- val SMARTSPACE = resourceBooleanFlag(402, R.bool.flag_smartspace, "smartspace")
// TODO(b/258517050): Clean up after the feature is launched.
@JvmField
- val SMARTSPACE_DATE_WEATHER_DECOUPLED = unreleasedFlag(403, "smartspace_date_weather_decoupled")
+ val SMARTSPACE_DATE_WEATHER_DECOUPLED =
+ sysPropBooleanFlag(403, "persist.sysui.ss.dw_decoupled", default = false)
// 500 - quick settings
@@ -248,17 +267,19 @@
// 600- status bar
// TODO(b/256614753): Tracking Bug
- val NEW_STATUS_BAR_MOBILE_ICONS = unreleasedFlag(606, "new_status_bar_mobile_icons")
+ val NEW_STATUS_BAR_MOBILE_ICONS =
+ unreleasedFlag(606, "new_status_bar_mobile_icons", teamfood = true)
// TODO(b/256614210): Tracking Bug
- val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon")
+ val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon", teamfood = true)
// TODO(b/256614751): Tracking Bug
val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND =
- unreleasedFlag(608, "new_status_bar_mobile_icons_backend")
+ unreleasedFlag(608, "new_status_bar_mobile_icons_backend", teamfood = true)
// TODO(b/256613548): Tracking Bug
- val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
+ val NEW_STATUS_BAR_WIFI_ICON_BACKEND =
+ unreleasedFlag(609, "new_status_bar_wifi_icon_backend", teamfood = true)
// TODO(b/256623670): Tracking Bug
@JvmField
@@ -284,7 +305,7 @@
// 801 - region sampling
// TODO(b/254512848): Tracking Bug
- val REGION_SAMPLING = unreleasedFlag(801, "region_sampling", teamfood = true)
+ val REGION_SAMPLING = unreleasedFlag(801, "region_sampling")
// 803 - screen contents translation
// TODO(b/254513187): Tracking Bug
@@ -297,7 +318,7 @@
// 900 - media
// TODO(b/254512697): Tracking Bug
- val MEDIA_TAP_TO_TRANSFER = unreleasedFlag(900, "media_tap_to_transfer", teamfood = true)
+ val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer")
// TODO(b/254512502): Tracking Bug
val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions")
@@ -317,8 +338,7 @@
// TODO(b/254513168): Tracking Bug
@JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
- @JvmField
- val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media", teamfood = true)
+ @JvmField val MEDIA_FALSING_PENALTY = releasedFlag(908, "media_falsing_media")
// TODO(b/261734857): Tracking Bug
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
@@ -327,13 +347,31 @@
val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+ // TODO(b/263512203): Tracking Bug
+ val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
+
+ // TODO(b/265813373): Tracking Bug
+ val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE =
+ unreleasedFlag(912, "media_ttt_dismiss_gesture", teamfood = true)
+
+ // TODO(b/266157412): Tracking Bug
+ val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
+
+ // TODO(b/266739309): Tracking Bug
+ @JvmField
+ val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update")
+
+ // TODO(b/267007629): Tracking Bug
+ val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress")
+
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
// TODO(b/254512758): Tracking Bug
@JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
- val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot")
+ // TODO(b/265045965): Tracking Bug
+ val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot")
// 1100 - windowing
@Keep
@@ -396,6 +434,29 @@
val WM_DESKTOP_WINDOWING_2 =
sysPropBooleanFlag(1112, "persist.wm.debug.desktop_mode_2", default = false)
+ // TODO(b/254513207): Tracking Bug to delete
+ @Keep
+ @JvmField
+ val WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES =
+ unreleasedFlag(
+ 1113,
+ name = "screen_record_enterprise_policies",
+ namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ teamfood = false
+ )
+
+ // TODO(b/198643358): Tracking bug
+ @Keep
+ @JvmField
+ val ENABLE_PIP_SIZE_LARGE_SCREEN =
+ sysPropBooleanFlag(1114, "persist.wm.debug.enable_pip_size_large_screen", default = false)
+
+ // TODO(b/265998256): Tracking bug
+ @Keep
+ @JvmField
+ val ENABLE_PIP_APP_ICON_OVERLAY =
+ sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = false)
+
// 1200 - predictive back
@Keep
@JvmField
@@ -405,7 +466,7 @@
@Keep
@JvmField
val WM_ENABLE_PREDICTIVE_BACK_ANIM =
- sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = false)
+ sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = true)
@Keep
@JvmField
@@ -430,15 +491,33 @@
val WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM =
unreleasedFlag(1206, "persist.wm.debug.predictive_back_bouncer_anim", teamfood = true)
- // 1300 - screenshots
- // TODO(b/254512719): Tracking Bug
- @JvmField val SCREENSHOT_REQUEST_PROCESSOR = releasedFlag(1300, "screenshot_request_processor")
+ // TODO(b/238475428): Tracking Bug
+ @JvmField
+ val WM_SHADE_ALLOW_BACK_GESTURE =
+ unreleasedFlag(1207, "persist.wm.debug.shade_allow_back_gesture", teamfood = false)
+ // TODO(b/238475428): Tracking Bug
+ @JvmField
+ val WM_SHADE_ANIMATE_BACK_GESTURE =
+ unreleasedFlag(1208, "persist.wm.debug.shade_animate_back_gesture", teamfood = false)
+
+ // TODO(b/265639042): Tracking Bug
+ @JvmField
+ val WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM =
+ unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
+
+ // 1300 - screenshots
// TODO(b/254513155): Tracking Bug
@JvmField
val SCREENSHOT_WORK_PROFILE_POLICY =
unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
+ // TODO(b/264916608): Tracking Bug
+ @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata")
+
+ // TODO(b/266955521): Tracking bug
+ @JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
+
// 1400 - columbus
// TODO(b/254512756): Tracking Bug
val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
@@ -447,34 +526,47 @@
val QUICK_TAP_FLOW_FRAMEWORK =
unreleasedFlag(1401, "quick_tap_flow_framework", teamfood = false)
- // 1500 - chooser
+ // 1500 - chooser aka sharesheet
// TODO(b/254512507): Tracking Bug
val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
+ // TODO(b/266983432) Tracking Bug
+ val SHARESHEET_CUSTOM_ACTIONS = unreleasedFlag(1501, "sharesheet_custom_actions")
+
+ // TODO(b/266982749) Tracking Bug
+ val SHARESHEET_RESELECTION_ACTION = unreleasedFlag(1502, "sharesheet_reselection_action")
+
+ // TODO(b/266983474) Tracking Bug
+ val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag(1503, "sharesheet_image_text_preview")
+
// 1600 - accessibility
@JvmField
val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
// 1700 - clipboard
- @JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor")
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// 1800 - shade container
@JvmField
- val LEAVE_SHADE_OPEN_FOR_BUGREPORT =
- unreleasedFlag(1800, "leave_shade_open_for_bugreport", teamfood = true)
+ val LEAVE_SHADE_OPEN_FOR_BUGREPORT = releasedFlag(1800, "leave_shade_open_for_bugreport")
+ // TODO(b/265944639): Tracking Bug
+ @JvmField val DUAL_SHADE = releasedFlag(1801, "dual_shade")
// 1900
@JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag")
// 2000 - device controls
- @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true)
+ @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels", teamfood = true)
@JvmField
val APP_PANELS_ALL_APPS_ALLOWED =
unreleasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true)
+ @JvmField
+ val CONTROLS_MANAGEMENT_NEW_FLOWS =
+ unreleasedFlag(2002, "controls_management_new_flows", teamfood = true)
+
// 2100 - Falsing Manager
@JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
@@ -489,6 +581,7 @@
@JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications")
+ @JvmField val ENABLE_STYLUS_EDUCATION = unreleasedFlag(2303, "enable_stylus_education")
// 2400 - performance tools and debugging info
// TODO(b/238923086): Tracking Bug
@@ -506,6 +599,22 @@
@JvmField
val OUTPUT_SWITCHER_DEVICE_STATUS = unreleasedFlag(2502, "output_switcher_device_status")
+ // TODO(b/20911786): Tracking Bug
+ @JvmField
+ val OUTPUT_SWITCHER_SHOW_API_ENABLED =
+ unreleasedFlag(2503, "output_switcher_show_api_enabled", teamfood = true)
+
+ // 2700 - unfold transitions
+ // TODO(b/265764985): Tracking Bug
+ @Keep
+ @JvmField
+ val ENABLE_DARK_VIGNETTE_WHEN_FOLDING =
+ unreleasedFlag(2700, "enable_dark_vignette_when_folding")
+
// TODO(b259590361): Tracking bug
val EXPERIMENTAL_FLAG = unreleasedFlag(2, "exp_flag_release")
+
+ // 2600 - keyboard shortcut
+ // TODO(b/259352579): Tracking Bug
+ @JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout")
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
index 8442230..0054d26 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
@@ -28,8 +28,8 @@
@JvmStatic
@Provides
@Named(ALL_FLAGS)
- fun providesAllFlags(): Map<Int, Flag<*>> {
- return FlagsFactory.knownFlags.map { it.value.id to it.value }.toMap()
+ fun providesAllFlags(): Map<String, Flag<*>> {
+ return FlagsFactory.knownFlags
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
index ae05c46..a02b795 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
@@ -54,10 +54,11 @@
return
}
+
for ((listener, flags) in listeners) {
propLoop@ for (propName in properties.keyset) {
for (flag in flags) {
- if (propName == getServerOverrideName(flag.id)) {
+ if (propName == getServerOverrideName(flag.id) || propName == flag.name) {
listener.onChange()
break@propLoop
}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
index d9bcb50..418aeca 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
@@ -37,9 +37,14 @@
private final int mId;
private String mOldClass;
- private ExtensionFragmentListener(View view, String tag, int id, Extension<T> extension) {
+ private ExtensionFragmentListener(
+ FragmentService fragmentService,
+ View view,
+ String tag,
+ int id,
+ Extension<T> extension) {
mTag = tag;
- mFragmentHostManager = FragmentHostManager.get(view);
+ mFragmentHostManager = fragmentService.getFragmentHostManager(view);
mExtension = extension;
mId = id;
mFragmentHostManager.getFragmentManager().beginTransaction()
@@ -60,8 +65,13 @@
mExtension.clearItem(true);
}
- public static <T> void attachExtensonToFragment(View view, String tag, int id,
+ public static <T> void attachExtensonToFragment(
+ FragmentService fragmentService,
+ View view,
+ String tag,
+ int id,
Extension<T> extension) {
- extension.addCallback(new ExtensionFragmentListener(view, tag, id, extension));
+ extension.addCallback(
+ new ExtensionFragmentListener(fragmentService, view, tag, id, extension));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
index 9c7411b..6a27ee7 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentHostManager.java
@@ -36,7 +36,6 @@
import androidx.annotation.NonNull;
import com.android.settingslib.applications.InterestingConfigChanges;
-import com.android.systemui.Dependency;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.util.leak.LeakDetector;
@@ -46,12 +45,17 @@
import java.util.ArrayList;
import java.util.HashMap;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
public class FragmentHostManager {
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Context mContext;
private final HashMap<String, ArrayList<FragmentListener>> mListeners = new HashMap<>();
private final View mRootView;
+ private final LeakDetector mLeakDetector;
private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_LOCALE
| ActivityInfo.CONFIG_ASSETS_PATHS);
@@ -61,14 +65,24 @@
private FragmentController mFragments;
private FragmentLifecycleCallbacks mLifecycleCallbacks;
- FragmentHostManager(FragmentService manager, View rootView) {
+ @AssistedInject
+ FragmentHostManager(
+ @Assisted View rootView,
+ FragmentService manager,
+ LeakDetector leakDetector) {
mContext = rootView.getContext();
mManager = manager;
mRootView = rootView;
+ mLeakDetector = leakDetector;
mConfigChanges.applyNewConfig(mContext.getResources());
createFragmentHost(null);
}
+ @AssistedFactory
+ public interface Factory {
+ FragmentHostManager create(View rootView);
+ }
+
private void createFragmentHost(Parcelable savedState) {
mFragments = FragmentController.createController(new HostCallbacks());
mFragments.attachHost(null);
@@ -86,7 +100,7 @@
@Override
public void onFragmentDestroyed(FragmentManager fm, Fragment f) {
- Dependency.get(LeakDetector.class).trackGarbage(f);
+ mLeakDetector.trackGarbage(f);
}
};
mFragments.getFragmentManager().registerFragmentLifecycleCallbacks(mLifecycleCallbacks,
@@ -211,19 +225,6 @@
}
}
- public static FragmentHostManager get(View view) {
- try {
- return Dependency.get(FragmentService.class).getFragmentHostManager(view);
- } catch (ClassCastException e) {
- // TODO: Some auto handling here?
- throw e;
- }
- }
-
- public static void removeAndDestroy(View view) {
- Dependency.get(FragmentService.class).removeAndDestroy(view);
- }
-
public void reloadFragments() {
Trace.beginSection("FrargmentHostManager#reloadFragments");
// Save the old state.
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
index fe945fb..d302b13a 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/FragmentService.java
@@ -53,6 +53,7 @@
*/
private final ArrayMap<String, FragmentInstantiationInfo> mInjectionMap = new ArrayMap<>();
private final Handler mHandler = new Handler();
+ private final FragmentHostManager.Factory mFragmentHostManagerFactory;
private ConfigurationController.ConfigurationListener mConfigurationListener =
new ConfigurationController.ConfigurationListener() {
@@ -67,8 +68,10 @@
@Inject
public FragmentService(
FragmentCreator.Factory fragmentCreatorFactory,
+ FragmentHostManager.Factory fragmentHostManagerFactory,
ConfigurationController configurationController,
DumpManager dumpManager) {
+ mFragmentHostManagerFactory = fragmentHostManagerFactory;
addFragmentInstantiationProvider(fragmentCreatorFactory.build());
configurationController.addCallback(mConfigurationListener);
@@ -152,7 +155,7 @@
public FragmentHostState(View view) {
mView = view;
- mFragmentHostManager = new FragmentHostManager(FragmentService.this, mView);
+ mFragmentHostManager = mFragmentHostManagerFactory.create(mView);
}
public void sendConfigurationChange(Configuration newConfig) {
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index db2cd91..e80e71c 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -21,7 +21,6 @@
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
-import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
@@ -901,7 +900,7 @@
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(EmergencyDialerConstants.EXTRA_ENTRY_TYPE,
EmergencyDialerConstants.ENTRY_TYPE_POWER_MENU);
- mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
}
}
}
@@ -959,8 +958,7 @@
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
- mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
+ mScreenshotHelper.takeScreenshot(SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index 4f1a2b3..ad7973e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -24,7 +24,6 @@
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.input.InputManager;
@@ -53,6 +52,7 @@
import com.android.systemui.CoreStartable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.util.settings.SecureSettings;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -108,6 +108,7 @@
protected volatile Context mContext;
private final Provider<LocalBluetoothManager> mBluetoothManagerProvider;
+ private final SecureSettings mSecureSettings;
private boolean mEnabled;
private String mKeyboardName;
@@ -125,9 +126,11 @@
private int mState;
@Inject
- public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider) {
+ public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider,
+ SecureSettings secureSettings) {
mContext = context;
this.mBluetoothManagerProvider = bluetoothManagerProvider;
+ mSecureSettings = secureSettings;
}
@Override
@@ -298,9 +301,8 @@
}
private boolean isUserSetupComplete() {
- ContentResolver resolver = mContext.getContentResolver();
- return Secure.getIntForUser(
- resolver, Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
+ return mSecureSettings.getIntForUser(
+ Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
}
private CachedBluetoothDevice getPairedKeyboard() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
index eaf1081..680c504 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt
@@ -31,10 +31,12 @@
import android.util.Log
import com.android.systemui.SystemUIAppComponentFactoryBase
import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking
class CustomizationProvider :
@@ -42,6 +44,7 @@
@Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
@Inject lateinit var previewManager: KeyguardRemotePreviewManager
+ @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher
private lateinit var contextAvailableCallback: ContextAvailableCallback
@@ -138,12 +141,14 @@
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor? {
- return when (uriMatcher.match(uri)) {
- MATCH_CODE_ALL_AFFORDANCES -> runBlocking { queryAffordances() }
- MATCH_CODE_ALL_SLOTS -> querySlots()
- MATCH_CODE_ALL_SELECTIONS -> runBlocking { querySelections() }
- MATCH_CODE_ALL_FLAGS -> queryFlags()
- else -> null
+ return runBlocking(mainDispatcher) {
+ when (uriMatcher.match(uri)) {
+ MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
+ MATCH_CODE_ALL_SLOTS -> querySlots()
+ MATCH_CODE_ALL_SELECTIONS -> querySelections()
+ MATCH_CODE_ALL_FLAGS -> queryFlags()
+ else -> null
+ }
}
}
@@ -282,6 +287,7 @@
.ENABLEMENT_ACTION_TEXT,
Contract.LockScreenQuickAffordances.AffordanceTable.Columns
.ENABLEMENT_COMPONENT_NAME,
+ Contract.LockScreenQuickAffordances.AffordanceTable.Columns.CONFIGURE_INTENT,
)
)
.apply {
@@ -298,6 +304,7 @@
),
representation.actionText,
representation.actionComponentName,
+ representation.configureIntent?.toUri(0),
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index f4a1227..47872d2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -17,7 +17,6 @@
package com.android.systemui.keyguard;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -80,6 +79,7 @@
import com.android.internal.policy.IKeyguardStateCallback;
import com.android.keyguard.mediator.ScreenOnCoordinator;
import com.android.systemui.SystemUIApplication;
+import com.android.systemui.settings.DisplayTracker;
import com.android.wm.shell.transition.ShellTransitions;
import com.android.wm.shell.transition.Transitions;
@@ -123,6 +123,7 @@
private final KeyguardLifecyclesDispatcher mKeyguardLifecyclesDispatcher;
private final ScreenOnCoordinator mScreenOnCoordinator;
private final ShellTransitions mShellTransitions;
+ private final DisplayTracker mDisplayTracker;
private static int newModeToLegacyMode(int newMode) {
switch (newMode) {
@@ -286,12 +287,14 @@
public KeyguardService(KeyguardViewMediator keyguardViewMediator,
KeyguardLifecyclesDispatcher keyguardLifecyclesDispatcher,
ScreenOnCoordinator screenOnCoordinator,
- ShellTransitions shellTransitions) {
+ ShellTransitions shellTransitions,
+ DisplayTracker displayTracker) {
super();
mKeyguardViewMediator = keyguardViewMediator;
mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
mScreenOnCoordinator = screenOnCoordinator;
mShellTransitions = shellTransitions;
+ mDisplayTracker = displayTracker;
}
@Override
@@ -328,7 +331,7 @@
unoccludeAnimationAdapter);
}
ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay(
- DEFAULT_DISPLAY, definition);
+ mDisplayTracker.getDefaultDisplayId(), definition);
return;
}
if (sEnableRemoteKeyguardGoingAwayAnimation) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index e644ed6..81da0bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -34,6 +34,7 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS;
+import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -122,6 +123,8 @@
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.dagger.KeyguardModule;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -507,6 +510,8 @@
private CentralSurfaces mCentralSurfaces;
+ private boolean mUnocclusionTransitionFlagEnabled = false;
+
private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener =
new DeviceConfig.OnPropertiesChangedListener() {
@Override
@@ -958,8 +963,9 @@
public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
- setOccluded(true /* isOccluded */, true /* animate */);
-
+ if (!mUnocclusionTransitionFlagEnabled) {
+ setOccluded(true /* isOccluded */, true /* animate */);
+ }
if (apps == null || apps.length == 0 || apps[0] == null) {
if (DEBUG) {
Log.d(TAG, "No apps provided to the OccludeByDream runner; "
@@ -1001,9 +1007,20 @@
applier.scheduleApply(paramsBuilder.build());
});
mOccludeByDreamAnimator.addListener(new AnimatorListenerAdapter() {
+ private boolean mIsCancelled = false;
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mIsCancelled = true;
+ }
+
@Override
public void onAnimationEnd(Animator animation) {
try {
+ if (!mIsCancelled && mUnocclusionTransitionFlagEnabled) {
+ // We're already on the main thread, don't queue this call
+ handleSetOccluded(true /* isOccluded */,
+ false /* animate */);
+ }
finishedCallback.onAnimationFinished();
mOccludeByDreamAnimator = null;
} catch (RemoteException e) {
@@ -1176,6 +1193,7 @@
ScreenOnCoordinator screenOnCoordinator,
InteractionJankMonitor interactionJankMonitor,
DreamOverlayStateController dreamOverlayStateController,
+ FeatureFlags featureFlags,
Lazy<ShadeController> shadeControllerLazy,
Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
@@ -1230,9 +1248,9 @@
R.dimen.physical_power_button_center_screen_location_y);
mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mDreamOpenAnimationDuration = context.getResources().getInteger(
- com.android.internal.R.integer.config_dreamOpenAnimationDuration);
+ mDreamOpenAnimationDuration = (int) DREAMING_ANIMATION_DURATION_MS;
mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS;
+ mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
}
public void userActivity() {
@@ -1316,7 +1334,7 @@
mHideAnimation = AnimationUtils.loadAnimation(mContext,
com.android.internal.R.anim.lock_screen_behind_enter);
- mWorkLockController = new WorkLockActivityController(mContext);
+ mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
}
@Override
@@ -1792,7 +1810,6 @@
Trace.beginSection("KeyguardViewMediator#setOccluded");
if (DEBUG) Log.d(TAG, "setOccluded " + isOccluded);
- mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD);
mHandler.removeMessages(SET_OCCLUDED);
Message msg = mHandler.obtainMessage(SET_OCCLUDED, isOccluded ? 1 : 0, animate ? 1 : 0);
mHandler.sendMessage(msg);
@@ -1825,6 +1842,8 @@
private void handleSetOccluded(boolean isOccluded, boolean animate) {
Trace.beginSection("KeyguardViewMediator#handleSetOccluded");
Log.d(TAG, "handleSetOccluded(" + isOccluded + ")");
+ mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_TRANSITION_FROM_AOD);
+
synchronized (KeyguardViewMediator.this) {
if (mHiding && isOccluded) {
// We're in the process of going away but WindowManager wants to show a
@@ -1893,12 +1912,6 @@
* Enable the keyguard if the settings are appropriate.
*/
private void doKeyguardLocked(Bundle options) {
- if (KeyguardUpdateMonitor.CORE_APPS_ONLY) {
- // Don't show keyguard during half-booted cryptkeeper stage.
- if (DEBUG) Log.d(TAG, "doKeyguard: not showing because booting to cryptkeeper");
- return;
- }
-
// if another app is disabling us, don't show
if (!mExternallyEnabled) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled");
@@ -1907,13 +1920,23 @@
return;
}
- // if the keyguard is already showing, don't bother. check flags in both files
- // to account for the hiding animation which results in a delay and discrepancy
- // between flags
+ // If the keyguard is already showing, see if we don't need to bother re-showing it. Check
+ // flags in both files to account for the hiding animation which results in a delay and
+ // discrepancy between flags.
if (mShowing && mKeyguardStateController.isShowing()) {
- if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
- resetStateLocked();
- return;
+ if (mPM.isInteractive()) {
+ // 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.
+ if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
+ resetStateLocked();
+ return;
+ } else {
+ // We are trying to show the keyguard while the screen is off - this results from
+ // race conditions involving locking while unlocking. Don't short-circuit here and
+ // ensure the keyguard is fully re-shown.
+ Log.e(TAG,
+ "doKeyguard: already showing, but re-showing since we're not interactive");
+ }
}
// In split system user mode, we never unlock system user.
@@ -2258,6 +2281,10 @@
}
if (!mKeyguardDonePending && mHideAnimationRun && !mHideAnimationRunning) {
handleKeyguardDone();
+ } else if (mSurfaceBehindRemoteAnimationRunning) {
+ // We're already running the keyguard exit animation, likely due to an in-progress swipe
+ // to unlock.
+ exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false /* cancelled */);
} else if (!mHideAnimationRun) {
if (DEBUG) Log.d(TAG, "tryKeyguardDone: starting pre-hide animation");
mHideAnimationRun = true;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
index 017b65a..ffd8a02 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
@@ -33,6 +33,7 @@
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.time.SystemClock;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -63,6 +64,7 @@
private final Context mContext;
private final DisplayMetrics mDisplayMetrics;
+ private final SystemClock mSystemClock;
@Nullable
private final IWallpaperManager mWallpaperManagerService;
@@ -71,6 +73,9 @@
private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN;
+ public static final long UNKNOWN_LAST_WAKE_TIME = -1;
+ private long mLastWakeTime = UNKNOWN_LAST_WAKE_TIME;
+
@Nullable
private Point mLastWakeOriginLocation = null;
@@ -84,10 +89,12 @@
public WakefulnessLifecycle(
Context context,
@Nullable IWallpaperManager wallpaperManagerService,
+ SystemClock systemClock,
DumpManager dumpManager) {
mContext = context;
mDisplayMetrics = context.getResources().getDisplayMetrics();
mWallpaperManagerService = wallpaperManagerService;
+ mSystemClock = systemClock;
dumpManager.registerDumpable(getClass().getSimpleName(), this);
}
@@ -104,6 +111,14 @@
}
/**
+ * Returns the most recent time (in device uptimeMillis) the display woke up.
+ * Returns {@link UNKNOWN_LAST_WAKE_TIME} if there hasn't been a wakeup yet.
+ */
+ public long getLastWakeTime() {
+ return mLastWakeTime;
+ }
+
+ /**
* Returns the most recent reason the device went to sleep up. This is one of
* PowerManager.GO_TO_SLEEP_REASON_*.
*/
@@ -117,6 +132,7 @@
}
setWakefulness(WAKEFULNESS_WAKING);
mLastWakeReason = pmWakeReason;
+ mLastWakeTime = mSystemClock.uptimeMillis();
updateLastWakeOriginLocation();
if (mWallpaperManagerService != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
index 16817ed..b92499e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
@@ -26,10 +26,10 @@
import android.content.Intent;
import android.os.Bundle;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -37,16 +37,20 @@
private static final String TAG = WorkLockActivityController.class.getSimpleName();
private final Context mContext;
+ private final UserTracker mUserTracker;
private final IActivityTaskManager mIatm;
- public WorkLockActivityController(Context context) {
- this(context, TaskStackChangeListeners.getInstance(), ActivityTaskManager.getService());
+ public WorkLockActivityController(Context context, UserTracker userTracker) {
+ this(context, userTracker, TaskStackChangeListeners.getInstance(),
+ ActivityTaskManager.getService());
}
@VisibleForTesting
WorkLockActivityController(
- Context context, TaskStackChangeListeners tscl, IActivityTaskManager iAtm) {
+ Context context, UserTracker userTracker, TaskStackChangeListeners tscl,
+ IActivityTaskManager iAtm) {
mContext = context;
+ mUserTracker = userTracker;
mIatm = iAtm;
tscl.registerTaskStackListener(mLockListener);
@@ -65,7 +69,8 @@
options.setLaunchTaskId(info.taskId);
options.setTaskOverlay(true, false /* canResume */);
- final int result = startActivityAsUser(intent, options.toBundle(), UserHandle.USER_CURRENT);
+ final int result = startActivityAsUser(intent, options.toBundle(),
+ mUserTracker.getUserId());
if (ActivityManager.isStartResultSuccessful(result)) {
// OK
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 47ef0fa..98d3570 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -39,6 +39,7 @@
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -112,6 +113,7 @@
ScreenOnCoordinator screenOnCoordinator,
InteractionJankMonitor interactionJankMonitor,
DreamOverlayStateController dreamOverlayStateController,
+ FeatureFlags featureFlags,
Lazy<ShadeController> shadeController,
Lazy<NotificationShadeWindowController> notificationShadeWindowController,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
@@ -142,6 +144,7 @@
screenOnCoordinator,
interactionJankMonitor,
dreamOverlayStateController,
+ featureFlags,
shadeController,
notificationShadeWindowController,
activityLaunchAnimator,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
index 80c6130..faeb485 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data
import android.view.KeyEvent
+import android.window.OnBackAnimationCallback
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.ActivityStarter
import java.lang.ref.WeakReference
@@ -51,4 +52,6 @@
cancelAction: Runnable?,
)
fun willDismissWithActions(): Boolean
+ /** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */
+ fun getBackCallback(): OnBackAnimationCallback
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index ea5b4f4..80675d3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -25,10 +25,13 @@
object BuiltInKeyguardQuickAffordanceKeys {
// Please keep alphabetical order of const names to simplify future maintenance.
const val CAMERA = "camera"
+ const val CREATE_NOTE = "create_note"
const val DO_NOT_DISTURB = "do_not_disturb"
const val FLASHLIGHT = "flashlight"
const val HOME_CONTROLS = "home"
+ const val MUTE = "mute"
const val QR_CODE_SCANNER = "qr_code_scanner"
const val QUICK_ACCESS_WALLET = "wallet"
+ const val VIDEO_CAMERA = "video_camera"
// Please keep alphabetical order of const names to simplify future maintenance.
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
index dbc376e..5a9f775 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
@@ -19,6 +19,7 @@
import android.app.StatusBarManager
import android.content.Context
+import android.content.pm.PackageManager
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.camera.CameraGestureHelper
@@ -36,6 +37,7 @@
@Inject
constructor(
@Application private val context: Context,
+ private val packageManager: PackageManager,
private val cameraGestureHelper: Lazy<CameraGestureHelper>,
) : KeyguardQuickAffordanceConfig {
@@ -46,7 +48,7 @@
get() = context.getString(R.string.accessibility_camera_button)
override val pickerIconResourceId: Int
- get() = com.android.internal.R.drawable.perm_group_camera
+ get() = R.drawable.ic_camera
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
get() =
@@ -54,12 +56,20 @@
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
icon =
Icon.Resource(
- com.android.internal.R.drawable.perm_group_camera,
+ R.drawable.ic_camera,
ContentDescription.Resource(R.string.accessibility_camera_button)
)
)
)
+ override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+ return if (isLaunchable()) {
+ super.getPickerScreenState()
+ } else {
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+ }
+ }
+
override fun onTriggered(
expandable: Expandable?
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
@@ -68,4 +78,8 @@
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
}
+
+ private fun isLaunchable(): Boolean {
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
index 8efb366..be73f85 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
+import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
@@ -39,6 +40,7 @@
import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -48,10 +50,10 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
-import javax.inject.Inject
@SysUISingleton
-class DoNotDisturbQuickAffordanceConfig constructor(
+class DoNotDisturbQuickAffordanceConfig
+constructor(
private val context: Context,
private val controller: ZenModeController,
private val secureSettings: SecureSettings,
@@ -59,7 +61,7 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val testConditionId: Uri?,
testDialog: EnableZenModeDialog?,
-): KeyguardQuickAffordanceConfig {
+) : KeyguardQuickAffordanceConfig {
@Inject
constructor(
@@ -76,20 +78,23 @@
private val conditionUri: Uri
get() =
- testConditionId ?: ZenModeConfig.toTimeCondition(
- context,
- settingsValue,
- userTracker.userId,
- true, /* shortVersion */
- ).id
+ testConditionId
+ ?: ZenModeConfig.toTimeCondition(
+ context,
+ settingsValue,
+ userTracker.userId,
+ true, /* shortVersion */
+ )
+ .id
private val dialog: EnableZenModeDialog by lazy {
- testDialog ?: EnableZenModeDialog(
- context,
- R.style.Theme_SystemUI_Dialog,
- true, /* cancelIsNeutral */
- ZenModeDialogMetricsLogger(context),
- )
+ testDialog
+ ?: EnableZenModeDialog(
+ context,
+ R.style.Theme_SystemUI_Dialog,
+ true, /* cancelIsNeutral */
+ ZenModeDialogMetricsLogger(context),
+ )
}
override val key: String = BuiltInKeyguardQuickAffordanceKeys.DO_NOT_DISTURB
@@ -98,58 +103,62 @@
override val pickerIconResourceId: Int = R.drawable.ic_do_not_disturb
- override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = combine(
- conflatedCallbackFlow {
- val callback = object: ZenModeController.Callback {
- override fun onZenChanged(zen: Int) {
- dndMode = zen
- trySendWithFailureLogging(updateState(), TAG)
- }
+ override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+ combine(
+ conflatedCallbackFlow {
+ val callback =
+ object : ZenModeController.Callback {
+ override fun onZenChanged(zen: Int) {
+ dndMode = zen
+ trySendWithFailureLogging(updateState(), TAG)
+ }
- override fun onZenAvailableChanged(available: Boolean) {
- isAvailable = available
- trySendWithFailureLogging(updateState(), TAG)
- }
- }
+ override fun onZenAvailableChanged(available: Boolean) {
+ isAvailable = available
+ trySendWithFailureLogging(updateState(), TAG)
+ }
+ }
- dndMode = controller.zen
- isAvailable = controller.isZenAvailable
- trySendWithFailureLogging(updateState(), TAG)
+ dndMode = controller.zen
+ isAvailable = controller.isZenAvailable
+ trySendWithFailureLogging(updateState(), TAG)
- controller.addCallback(callback)
+ controller.addCallback(callback)
- awaitClose { controller.removeCallback(callback) }
- },
- secureSettings
- .observerFlow(Settings.Secure.ZEN_DURATION)
- .onStart { emit(Unit) }
- .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
- .flowOn(backgroundDispatcher)
- .distinctUntilChanged()
- .onEach { settingsValue = it }
- ) { callbackFlowValue, _ -> callbackFlowValue }
+ awaitClose { controller.removeCallback(callback) }
+ },
+ secureSettings
+ .observerFlow(userTracker.userId, Settings.Secure.ZEN_DURATION)
+ .onStart { emit(Unit) }
+ .map { secureSettings.getInt(Settings.Secure.ZEN_DURATION, ZEN_MODE_OFF) }
+ .flowOn(backgroundDispatcher)
+ .distinctUntilChanged()
+ .onEach { settingsValue = it }
+ ) { callbackFlowValue, _ -> callbackFlowValue }
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
return if (controller.isZenAvailable) {
- KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ KeyguardQuickAffordanceConfig.PickerScreenState.Default(
+ configureIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
+ )
} else {
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
}
}
- override fun onTriggered(expandable: Expandable?):
- KeyguardQuickAffordanceConfig.OnTriggeredResult {
+ override fun onTriggered(
+ expandable: Expandable?
+ ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
return when {
- !isAvailable ->
- KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ !isAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
dndMode != ZEN_MODE_OFF -> {
controller.setZen(ZEN_MODE_OFF, null, TAG)
KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
}
settingsValue == ZEN_DURATION_PROMPT ->
KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog(
- dialog.createDialog(),
- expandable
+ dialog.createDialog(),
+ expandable
)
settingsValue == ZEN_DURATION_FOREVER -> {
controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG)
@@ -187,4 +196,4 @@
companion object {
const val TAG = "DoNotDisturbQuickAffordanceConfig"
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
index 62fe80a..3412f35 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
@@ -135,7 +135,7 @@
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
if (flashlightController.isAvailable) {
- KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ KeyguardQuickAffordanceConfig.PickerScreenState.Default()
} else {
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 09e5ec0..a1e9137d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -90,7 +90,7 @@
)
}
- return KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ return KeyguardQuickAffordanceConfig.PickerScreenState.Default()
}
override fun onTriggered(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index 71d01eb..4556195 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -33,20 +33,24 @@
@Provides
@ElementsIntoSet
fun quickAffordanceConfigs(
+ camera: CameraQuickAffordanceConfig,
doNotDisturb: DoNotDisturbQuickAffordanceConfig,
flashlight: FlashlightQuickAffordanceConfig,
home: HomeControlsKeyguardQuickAffordanceConfig,
+ mute: MuteQuickAffordanceConfig,
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
- camera: CameraQuickAffordanceConfig,
+ videoCamera: VideoCameraQuickAffordanceConfig,
): Set<KeyguardQuickAffordanceConfig> {
return setOf(
camera,
doNotDisturb,
flashlight,
home,
+ mute,
quickAccessWallet,
qrCodeScanner,
+ videoCamera,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 20588e9..e32edcb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -46,7 +46,7 @@
* Returns the [PickerScreenState] representing the affordance in the settings or selector
* experience.
*/
- suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default
+ suspend fun getPickerScreenState(): PickerScreenState = PickerScreenState.Default()
/**
* Notifies that the affordance was clicked by the user.
@@ -63,7 +63,10 @@
sealed class PickerScreenState {
/** The picker shows the item for selecting this affordance as it normally would. */
- object Default : PickerScreenState()
+ data class Default(
+ /** Optional [Intent] to use to start an activity to configure this affordance. */
+ val configureIntent: Intent? = null,
+ ) : PickerScreenState()
/**
* The picker does not show an item for selecting this affordance as it is not supported on
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
new file mode 100644
index 0000000..d085db9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import javax.inject.Inject
+
+@SysUISingleton
+class MuteQuickAffordanceConfig @Inject constructor(
+ context: Context,
+ private val userTracker: UserTracker,
+ private val userFileManager: UserFileManager,
+ private val ringerModeTracker: RingerModeTracker,
+ private val audioManager: AudioManager,
+) : KeyguardQuickAffordanceConfig {
+
+ private var previousNonSilentMode: Int = DEFAULT_LAST_NON_SILENT_VALUE
+
+ override val key: String = BuiltInKeyguardQuickAffordanceKeys.MUTE
+
+ override val pickerName: String = context.getString(R.string.volume_ringer_status_silent)
+
+ override val pickerIconResourceId: Int = R.drawable.ic_notifications_silence
+
+ override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+ ringerModeTracker.ringerModeInternal.asFlow()
+ .onStart { emit(getLastNonSilentRingerMode()) }
+ .distinctUntilChanged()
+ .onEach { mode ->
+ // only remember last non-SILENT ringer mode
+ if (mode != null && mode != AudioManager.RINGER_MODE_SILENT) {
+ previousNonSilentMode = mode
+ }
+ }
+ .map { mode ->
+ val (activationState, contentDescriptionRes) = when {
+ audioManager.isVolumeFixed ->
+ ActivationState.NotSupported to
+ R.string.volume_ringer_hint_mute
+ mode == AudioManager.RINGER_MODE_SILENT ->
+ ActivationState.Active to
+ R.string.volume_ringer_hint_mute
+ else ->
+ ActivationState.Inactive to
+ R.string.volume_ringer_hint_unmute
+ }
+
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.ic_notifications_silence,
+ ContentDescription.Resource(contentDescriptionRes),
+ ),
+ activationState,
+ )
+ }
+
+ override fun onTriggered(
+ expandable: Expandable?
+ ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+ val newRingerMode: Int
+ val currentRingerMode =
+ ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+ if (currentRingerMode == AudioManager.RINGER_MODE_SILENT) {
+ newRingerMode = previousNonSilentMode
+ } else {
+ previousNonSilentMode = currentRingerMode
+ newRingerMode = AudioManager.RINGER_MODE_SILENT
+ }
+
+ if (currentRingerMode != newRingerMode) {
+ audioManager.ringerModeInternal = newRingerMode
+ }
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
+
+ override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
+ if (audioManager.isVolumeFixed) {
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+ } else {
+ KeyguardQuickAffordanceConfig.PickerScreenState.Default()
+ }
+
+ /**
+ * Gets the last non-silent ringer mode from shared-preferences if it exists. This is
+ * cached by [MuteQuickAffordanceCoreStartable] while this affordance is selected
+ */
+ private fun getLastNonSilentRingerMode(): Int =
+ userFileManager.getSharedPreferences(
+ MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+ Context.MODE_PRIVATE,
+ userTracker.userId
+ ).getInt(
+ LAST_NON_SILENT_RINGER_MODE_KEY,
+ ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE
+ )
+
+ private fun <T> LiveData<T>.asFlow(): Flow<T?> =
+ conflatedCallbackFlow {
+ val observer = Observer { value: T -> trySend(value) }
+ observeForever(observer)
+ send(value)
+ awaitClose { removeObserver(observer) }
+ }
+
+ companion object {
+ const val LAST_NON_SILENT_RINGER_MODE_KEY = "key_last_non_silent_ringer_mode"
+ const val MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME = "quick_affordance_mute_ringer_mode_cache"
+ private const val DEFAULT_LAST_NON_SILENT_VALUE = AudioManager.RINGER_MODE_NORMAL
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
new file mode 100644
index 0000000..12a6310
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.Observer
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+/**
+ * Store previous non-silent Ringer Mode into shared prefs to be used for Mute Lockscreen Shortcut
+ */
+@SysUISingleton
+class MuteQuickAffordanceCoreStartable @Inject constructor(
+ private val featureFlags: FeatureFlags,
+ private val userTracker: UserTracker,
+ private val ringerModeTracker: RingerModeTracker,
+ private val userFileManager: UserFileManager,
+ private val keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository,
+ @Application private val coroutineScope: CoroutineScope,
+) : CoreStartable {
+
+ private val observer = Observer(this::updateLastNonSilentRingerMode)
+
+ override fun start() {
+ if (!featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)) return
+
+ // only listen to ringerModeInternal changes when Mute is one of the selected affordances
+ keyguardQuickAffordanceRepository
+ .selections
+ .map { selections ->
+ // determines if Mute is selected in any lockscreen shortcut position
+ val muteSelected: Boolean = selections.values.any { configList ->
+ configList.any { config ->
+ config.key == BuiltInKeyguardQuickAffordanceKeys.MUTE
+ }
+ }
+ if (muteSelected) {
+ ringerModeTracker.ringerModeInternal.observeForever(observer)
+ } else {
+ ringerModeTracker.ringerModeInternal.removeObserver(observer)
+ }
+ }
+ .launchIn(coroutineScope)
+ }
+
+ private fun updateLastNonSilentRingerMode(lastRingerMode: Int) {
+ if (AudioManager.RINGER_MODE_SILENT != lastRingerMode) {
+ userFileManager.getSharedPreferences(
+ MuteQuickAffordanceConfig.MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME,
+ Context.MODE_PRIVATE,
+ userTracker.userId
+ )
+ .edit()
+ .putInt(MuteQuickAffordanceConfig.LAST_NON_SILENT_RINGER_MODE_KEY, lastRingerMode)
+ .apply()
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index 4f7990f..ea6c107 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -89,7 +89,7 @@
),
),
)
- else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 1928f40..4ba2eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -100,9 +100,9 @@
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
return when {
- !walletController.isWalletEnabled ->
+ !walletController.walletClient.isWalletServiceAvailable ->
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
- walletController.walletClient.tileIcon == null || queryCards().isEmpty() -> {
+ !walletController.isWalletEnabled || queryCards().isEmpty() -> {
val componentName =
walletController.walletClient.createWalletSettingsIntent().toComponentName()
val actionText =
@@ -128,7 +128,7 @@
actionComponentName = componentName,
)
}
- else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
new file mode 100644
index 0000000..d9ec3b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfig.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.data.quickaffordance
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.camera.CameraIntents
+import com.android.systemui.camera.CameraIntentsWrapper
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+@SysUISingleton
+class VideoCameraQuickAffordanceConfig
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val cameraIntents: CameraIntentsWrapper,
+ private val activityIntentHelper: ActivityIntentHelper,
+ private val userTracker: UserTracker,
+) : KeyguardQuickAffordanceConfig {
+
+ private val intent: Intent by lazy {
+ cameraIntents.getVideoCameraIntent().apply {
+ putExtra(
+ CameraIntents.EXTRA_LAUNCH_SOURCE,
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE,
+ )
+ }
+ }
+
+ override val key: String
+ get() = BuiltInKeyguardQuickAffordanceKeys.VIDEO_CAMERA
+
+ override val pickerName: String
+ get() = context.getString(R.string.video_camera)
+
+ override val pickerIconResourceId: Int
+ get() = R.drawable.ic_videocam
+
+ override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
+ get() =
+ flowOf(
+ if (isLaunchable()) {
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ icon =
+ Icon.Resource(
+ R.drawable.ic_videocam,
+ ContentDescription.Resource(R.string.video_camera)
+ )
+ )
+ } else {
+ KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+ }
+ )
+
+ override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
+ return if (isLaunchable()) {
+ super.getPickerScreenState()
+ } else {
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+ }
+ }
+
+ override fun onTriggered(
+ expandable: Expandable?
+ ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
+ intent = intent,
+ canShowWhileLocked = false,
+ )
+ }
+
+ private fun isLaunchable(): Boolean {
+ return activityIntentHelper.getTargetActivityInfo(
+ intent,
+ userTracker.userId,
+ true,
+ ) != null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
new file mode 100644
index 0000000..0af596a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
+import android.content.Context
+import android.content.IntentFilter
+import android.os.Looper
+import android.os.UserHandle
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
+
+/**
+ * Acts as source of truth for biometric authentication related settings like enrollments, device
+ * policy, etc.
+ *
+ * Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about
+ * upstream changes.
+ */
+interface BiometricSettingsRepository {
+ /** Whether any fingerprints are enrolled for the current user. */
+ val isFingerprintEnrolled: StateFlow<Boolean>
+
+ /**
+ * Whether the current user is allowed to use a strong biometric for device entry based on
+ * Android Security policies. If false, the user may be able to use primary authentication for
+ * device entry.
+ */
+ val isStrongBiometricAllowed: StateFlow<Boolean>
+
+ /** Whether fingerprint feature is enabled for the current user by the DevicePolicy */
+ val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean>
+}
+
+@SysUISingleton
+class BiometricSettingsRepositoryImpl
+@Inject
+constructor(
+ context: Context,
+ lockPatternUtils: LockPatternUtils,
+ broadcastDispatcher: BroadcastDispatcher,
+ authController: AuthController,
+ userRepository: UserRepository,
+ devicePolicyManager: DevicePolicyManager,
+ @Application scope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ @Main looper: Looper,
+) : BiometricSettingsRepository {
+
+ /** UserId of the current selected user. */
+ private val selectedUserId: Flow<Int> =
+ userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged()
+
+ override val isFingerprintEnrolled: StateFlow<Boolean> =
+ selectedUserId
+ .flatMapLatest {
+ conflatedCallbackFlow {
+ val callback =
+ object : AuthController.Callback {
+ override fun onEnrollmentsChanged(
+ sensorBiometricType: BiometricType,
+ userId: Int,
+ hasEnrollments: Boolean
+ ) {
+ if (sensorBiometricType.isFingerprint) {
+ trySendWithFailureLogging(
+ hasEnrollments,
+ TAG,
+ "update fpEnrollment"
+ )
+ }
+ }
+ }
+ authController.addCallback(callback)
+ awaitClose { authController.removeCallback(callback) }
+ }
+ }
+ .stateIn(
+ scope,
+ started = SharingStarted.Eagerly,
+ initialValue =
+ authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id)
+ )
+
+ override val isStrongBiometricAllowed: StateFlow<Boolean> =
+ selectedUserId
+ .flatMapLatest { currUserId ->
+ conflatedCallbackFlow {
+ val callback =
+ object : LockPatternUtils.StrongAuthTracker(context, looper) {
+ override fun onStrongAuthRequiredChanged(userId: Int) {
+ if (currUserId != userId) {
+ return
+ }
+
+ trySendWithFailureLogging(
+ isBiometricAllowedForUser(true, currUserId),
+ TAG
+ )
+ }
+
+ override fun onIsNonStrongBiometricAllowedChanged(userId: Int) {
+ // no-op
+ }
+ }
+ lockPatternUtils.registerStrongAuthTracker(callback)
+ awaitClose { lockPatternUtils.unregisterStrongAuthTracker(callback) }
+ }
+ }
+ .stateIn(
+ scope,
+ started = SharingStarted.Eagerly,
+ initialValue =
+ lockPatternUtils.isBiometricAllowedForUser(
+ userRepository.getSelectedUserInfo().id
+ )
+ )
+
+ override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> =
+ selectedUserId
+ .flatMapLatest { userId ->
+ broadcastDispatcher
+ .broadcastFlow(
+ filter = IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED),
+ user = UserHandle.ALL
+ )
+ .transformLatest {
+ emit(
+ (devicePolicyManager.getKeyguardDisabledFeatures(null, userId) and
+ DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) == 0
+ )
+ }
+ .flowOn(backgroundDispatcher)
+ .distinctUntilChanged()
+ }
+ .stateIn(
+ scope,
+ started = SharingStarted.Eagerly,
+ initialValue =
+ devicePolicyManager.getKeyguardDisabledFeatures(
+ null,
+ userRepository.getSelectedUserInfo().id
+ ) and DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT == 0
+ )
+
+ companion object {
+ private const val TAG = "BiometricsRepositoryImpl"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt
new file mode 100644
index 0000000..93c9781
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+enum class BiometricType(val isFingerprint: Boolean) {
+ // An unsupported biometric type
+ UNKNOWN(false),
+
+ // Fingerprint sensor that is located on the back (opposite side of the display) of the device
+ REAR_FINGERPRINT(true),
+
+ // Fingerprint sensor that is located under the display
+ UNDER_DISPLAY_FINGERPRINT(true),
+
+ // Fingerprint sensor that is located on the side of the device, typically on the power button
+ SIDE_FINGERPRINT(true),
+ FACE(false),
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
new file mode 100644
index 0000000..b3a9cf5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.data.repository
+
+import android.hardware.biometrics.BiometricSourceType
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Encapsulates state about device entry fingerprint auth mechanism. */
+interface DeviceEntryFingerprintAuthRepository {
+ /** Whether the device entry fingerprint auth is locked out. */
+ val isLockedOut: StateFlow<Boolean>
+}
+
+/**
+ * Implementation of [DeviceEntryFingerprintAuthRepository] that uses [KeyguardUpdateMonitor] as the
+ * source of truth.
+ *
+ * Dependency on [KeyguardUpdateMonitor] will be removed once fingerprint auth state is moved out of
+ * [KeyguardUpdateMonitor]
+ */
+@SysUISingleton
+class DeviceEntryFingerprintAuthRepositoryImpl
+@Inject
+constructor(
+ val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ @Application scope: CoroutineScope,
+) : DeviceEntryFingerprintAuthRepository {
+
+ override val isLockedOut: StateFlow<Boolean> =
+ conflatedCallbackFlow {
+ val sendLockoutUpdate =
+ fun() {
+ trySendWithFailureLogging(
+ keyguardUpdateMonitor.isFingerprintLockedOut,
+ TAG,
+ "onLockedOutStateChanged"
+ )
+ }
+ val callback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onLockedOutStateChanged(
+ biometricSourceType: BiometricSourceType?
+ ) {
+ if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
+ sendLockoutUpdate()
+ }
+ }
+ }
+ keyguardUpdateMonitor.registerCallback(callback)
+ sendLockoutUpdate()
+ awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+ }
+ .stateIn(scope, started = SharingStarted.Eagerly, initialValue = false)
+
+ companion object {
+ const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index 90f3c7d..4ac6ac8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -20,15 +20,17 @@
import com.android.keyguard.ViewMediatorCallback
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
import com.android.systemui.log.dagger.BouncerLog
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
-import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
@@ -39,30 +41,16 @@
*
* Make sure to add newly added flows to the logger.
*/
-@SysUISingleton
-class KeyguardBouncerRepository
-@Inject
-constructor(
- private val viewMediatorCallback: ViewMediatorCallback,
- @Application private val applicationScope: CoroutineScope,
- @BouncerLog private val buffer: TableLogBuffer,
-) {
+interface KeyguardBouncerRepository {
/** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
- private val _primaryBouncerVisible = MutableStateFlow(false)
- val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
- private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
- val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
- private val _primaryBouncerShowingSoon = MutableStateFlow(false)
- val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
- private val _primaryBouncerHide = MutableStateFlow(false)
- val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
- private val _primaryBouncerStartingToHide = MutableStateFlow(false)
- val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
- private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
- val primaryBouncerStartingDisappearAnimation = _primaryBouncerDisappearAnimation.asStateFlow()
+ val primaryBouncerVisible: StateFlow<Boolean>
+ val primaryBouncerShow: StateFlow<KeyguardBouncerModel?>
+ val primaryBouncerShowingSoon: StateFlow<Boolean>
+ val primaryBouncerHide: StateFlow<Boolean>
+ val primaryBouncerStartingToHide: StateFlow<Boolean>
+ val primaryBouncerStartingDisappearAnimation: StateFlow<Runnable?>
/** Determines if we want to instantaneously show the primary bouncer instead of translating. */
- private val _primaryBouncerScrimmed = MutableStateFlow(false)
- val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+ val primaryBouncerScrimmed: StateFlow<Boolean>
/**
* Set how much of the notification panel is showing on the screen.
* ```
@@ -70,83 +58,184 @@
* 1f = panel fully showing = bouncer fully hidden
* ```
*/
- private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncer.EXPANSION_HIDDEN)
- val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
+ val panelExpansionAmount: StateFlow<Float>
+ val keyguardPosition: StateFlow<Float>
+ val onScreenTurnedOff: StateFlow<Boolean>
+ val isBackButtonEnabled: StateFlow<Boolean?>
+ /** Determines if user is already unlocked */
+ val keyguardAuthenticated: StateFlow<Boolean?>
+ val showMessage: StateFlow<BouncerShowMessageModel?>
+ val resourceUpdateRequests: StateFlow<Boolean>
+ val bouncerPromptReason: Int
+ val bouncerErrorMessage: CharSequence?
+ val isAlternateBouncerVisible: StateFlow<Boolean>
+ val isAlternateBouncerUIAvailable: StateFlow<Boolean>
+ var lastAlternateBouncerVisibleTime: Long
+
+ fun setPrimaryScrimmed(isScrimmed: Boolean)
+
+ fun setPrimaryVisible(isVisible: Boolean)
+
+ fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?)
+
+ fun setPrimaryShowingSoon(showingSoon: Boolean)
+
+ fun setPrimaryHide(hide: Boolean)
+
+ fun setPrimaryStartingToHide(startingToHide: Boolean)
+
+ fun setPrimaryStartDisappearAnimation(runnable: Runnable?)
+
+ fun setPanelExpansion(panelExpansion: Float)
+
+ fun setKeyguardPosition(keyguardPosition: Float)
+
+ fun setResourceUpdateRequests(willUpdateResources: Boolean)
+
+ fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?)
+
+ fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?)
+
+ fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean)
+
+ fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean)
+
+ fun setAlternateVisible(isVisible: Boolean)
+
+ fun setAlternateBouncerUIAvailable(isAvailable: Boolean)
+}
+
+@SysUISingleton
+class KeyguardBouncerRepositoryImpl
+@Inject
+constructor(
+ private val viewMediatorCallback: ViewMediatorCallback,
+ private val clock: SystemClock,
+ @Application private val applicationScope: CoroutineScope,
+ @BouncerLog private val buffer: TableLogBuffer,
+) : KeyguardBouncerRepository {
+ /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
+ private val _primaryBouncerVisible = MutableStateFlow(false)
+ override val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+ private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
+ override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+ private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+ override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+ private val _primaryBouncerHide = MutableStateFlow(false)
+ override val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+ private val _primaryBouncerStartingToHide = MutableStateFlow(false)
+ override val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+ private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
+ override val primaryBouncerStartingDisappearAnimation =
+ _primaryBouncerDisappearAnimation.asStateFlow()
+ /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
+ private val _primaryBouncerScrimmed = MutableStateFlow(false)
+ override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+ /**
+ * Set how much of the notification panel is showing on the screen.
+ * ```
+ * 0f = panel fully hidden = bouncer fully showing
+ * 1f = panel fully showing = bouncer fully hidden
+ * ```
+ */
+ private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
+ override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
private val _keyguardPosition = MutableStateFlow(0f)
- val keyguardPosition = _keyguardPosition.asStateFlow()
+ override val keyguardPosition = _keyguardPosition.asStateFlow()
private val _onScreenTurnedOff = MutableStateFlow(false)
- val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+ override val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
- val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+ override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
/** Determines if user is already unlocked */
- val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+ override val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
- val showMessage = _showMessage.asStateFlow()
+ override val showMessage = _showMessage.asStateFlow()
private val _resourceUpdateRequests = MutableStateFlow(false)
- val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
- val bouncerPromptReason: Int
+ override val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+ override val bouncerPromptReason: Int
get() = viewMediatorCallback.bouncerPromptReason
- val bouncerErrorMessage: CharSequence?
+ override val bouncerErrorMessage: CharSequence?
get() = viewMediatorCallback.consumeCustomMessage()
init {
setUpLogging()
}
- fun setPrimaryScrimmed(isScrimmed: Boolean) {
+ /** Values associated with the AlternateBouncer */
+ private val _isAlternateBouncerVisible = MutableStateFlow(false)
+ override val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow()
+ override var lastAlternateBouncerVisibleTime: Long = NOT_VISIBLE
+ private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false)
+ override val isAlternateBouncerUIAvailable: StateFlow<Boolean> =
+ _isAlternateBouncerUIAvailable.asStateFlow()
+
+ override fun setPrimaryScrimmed(isScrimmed: Boolean) {
_primaryBouncerScrimmed.value = isScrimmed
}
- fun setPrimaryVisible(isVisible: Boolean) {
+ override fun setPrimaryVisible(isVisible: Boolean) {
_primaryBouncerVisible.value = isVisible
}
- fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+ override fun setAlternateVisible(isVisible: Boolean) {
+ if (isVisible && !_isAlternateBouncerVisible.value) {
+ lastAlternateBouncerVisibleTime = clock.uptimeMillis()
+ } else if (!isVisible) {
+ lastAlternateBouncerVisibleTime = NOT_VISIBLE
+ }
+ _isAlternateBouncerVisible.value = isVisible
+ }
+
+ override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+ _isAlternateBouncerUIAvailable.value = isAvailable
+ }
+
+ override fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
_primaryBouncerShow.value = keyguardBouncerModel
}
- fun setPrimaryShowingSoon(showingSoon: Boolean) {
+ override fun setPrimaryShowingSoon(showingSoon: Boolean) {
_primaryBouncerShowingSoon.value = showingSoon
}
- fun setPrimaryHide(hide: Boolean) {
+ override fun setPrimaryHide(hide: Boolean) {
_primaryBouncerHide.value = hide
}
- fun setPrimaryStartingToHide(startingToHide: Boolean) {
+ override fun setPrimaryStartingToHide(startingToHide: Boolean) {
_primaryBouncerStartingToHide.value = startingToHide
}
- fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+ override fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
_primaryBouncerDisappearAnimation.value = runnable
}
- fun setPanelExpansion(panelExpansion: Float) {
+ override fun setPanelExpansion(panelExpansion: Float) {
_panelExpansionAmount.value = panelExpansion
}
- fun setKeyguardPosition(keyguardPosition: Float) {
+ override fun setKeyguardPosition(keyguardPosition: Float) {
_keyguardPosition.value = keyguardPosition
}
- fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+ override fun setResourceUpdateRequests(willUpdateResources: Boolean) {
_resourceUpdateRequests.value = willUpdateResources
}
- fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+ override fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
_showMessage.value = bouncerShowMessageModel
}
- fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+ override fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
_keyguardAuthenticated.value = keyguardAuthenticated
}
- fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+ override fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
_isBackButtonEnabled.value = isBackButtonEnabled
}
- fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
+ override fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
_onScreenTurnedOff.value = onScreenTurnedOff
}
@@ -202,4 +291,8 @@
.logDiffsForTable(buffer, "", "ResourceUpdateRequests", false)
.launchIn(applicationScope)
}
+
+ companion object {
+ private const val NOT_VISIBLE = -1L
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index e3f5e90..8ece318 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -146,7 +146,7 @@
* Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
* slot with the given ID. The configs are sorted in descending priority order.
*/
- fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
+ fun getCurrentSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
return configs.filter { selections.contains(it.key) }
}
@@ -155,7 +155,7 @@
* Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
* are sorted in descending priority order.
*/
- fun getSelections(): Map<String, List<String>> {
+ fun getCurrentSelections(): Map<String, List<String>> {
return selectionManager.value.getSelections()
}
@@ -187,6 +187,8 @@
pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
}
.map { (config, pickerState) ->
+ val defaultPickerState =
+ pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Default
val disabledPickerState =
pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
KeyguardQuickAffordancePickerRepresentation(
@@ -198,6 +200,7 @@
instructions = disabledPickerState?.instructions,
actionText = disabledPickerState?.actionText,
actionComponentName = disabledPickerState?.actionComponentName,
+ configureIntent = defaultPickerState?.configureIntent,
)
}
}
@@ -214,7 +217,7 @@
private inner class Dumpster : Dumpable {
override fun dump(pw: PrintWriter, args: Array<out String>) {
val slotPickerRepresentations = getSlotPickerRepresentations()
- val selectionsBySlotId = getSelections()
+ val selectionsBySlotId = getCurrentSelections()
pw.println("Slots & selections:")
slotPickerRepresentations.forEach { slotPickerRepresentation ->
val slotId = slotPickerRepresentation.id
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 a4fd087..db95562 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
@@ -40,6 +40,7 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
+import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
@@ -88,6 +89,9 @@
/** Observable for whether the bouncer is showing. */
val isBouncerShowing: Flow<Boolean>
+ /** Is the always-on display available to be used? */
+ val isAodAvailable: Flow<Boolean>
+
/**
* Observable for whether we are in doze state.
*
@@ -144,6 +148,9 @@
/** Source of the most recent biometric unlock, such as fingerprint or face. */
val biometricUnlockSource: Flow<BiometricUnlockSource?>
+ /** Whether quick settings or quick-quick settings is visible. */
+ val isQuickSettingsVisible: Flow<Boolean>
+
/**
* Returns `true` if the keyguard is showing; `false` otherwise.
*
@@ -168,6 +175,9 @@
* Returns whether the keyguard bottom area should be constrained to the top of the lock icon
*/
fun isUdfpsSupported(): Boolean
+
+ /** Sets whether quick settings or quick-quick settings is visible. */
+ fun setQuickSettingsVisible(isVisible: Boolean)
}
/** Encapsulates application state for the keyguard. */
@@ -182,6 +192,7 @@
private val keyguardStateController: KeyguardStateController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dozeTransitionListener: DozeTransitionListener,
+ private val dozeParameters: DozeParameters,
private val authController: AuthController,
private val dreamOverlayCallbackController: DreamOverlayCallbackController,
) : KeyguardRepository {
@@ -220,6 +231,31 @@
}
.distinctUntilChanged()
+ override val isAodAvailable: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : DozeParameters.Callback {
+ override fun onAlwaysOnChange() {
+ trySendWithFailureLogging(
+ dozeParameters.getAlwaysOn(),
+ TAG,
+ "updated isAodAvailable"
+ )
+ }
+ }
+
+ dozeParameters.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ dozeParameters.getAlwaysOn(),
+ TAG,
+ "initial isAodAvailable"
+ )
+
+ awaitClose { dozeParameters.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
+
override val isKeyguardOccluded: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
@@ -551,6 +587,9 @@
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
+ private val _isQuickSettingsVisible = MutableStateFlow(false)
+ override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
@@ -565,6 +604,10 @@
override fun isUdfpsSupported(): Boolean = keyguardUpdateMonitor.isUdfpsSupported
+ override fun setQuickSettingsVisible(isVisible: Boolean) {
+ _isQuickSettingsVisible.value = isVisible
+ }
+
private fun statusBarStateIntToObject(value: Int): StatusBarState {
return when (value) {
0 -> StatusBarState.SHADE
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index 26f853f..4a262f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -30,4 +30,17 @@
@Binds
fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository
+
+ @Binds
+ fun biometricSettingsRepository(
+ impl: BiometricSettingsRepositoryImpl
+ ): BiometricSettingsRepository
+
+ @Binds
+ fun deviceEntryFingerprintAuthRepository(
+ impl: DeviceEntryFingerprintAuthRepositoryImpl
+ ): DeviceEntryFingerprintAuthRepository
+
+ @Binds
+ fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository
}
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 343c2dc..0c4bca6 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
@@ -135,11 +135,14 @@
Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
return null
}
- if (lastStep.transitionState != TransitionState.FINISHED) {
- Log.i(TAG, "Transition still active: $lastStep, canceling")
- }
+ val startingValue =
+ if (lastStep.transitionState != TransitionState.FINISHED) {
+ Log.i(TAG, "Transition still active: $lastStep, canceling")
+ lastStep.value
+ } else {
+ 0f
+ }
- val startingValue = 1f - lastStep.value
lastAnimator?.cancel()
lastAnimator = info.animator
@@ -206,7 +209,7 @@
return
}
- if (state == TransitionState.FINISHED) {
+ if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
updateTransitionId = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
new file mode 100644
index 0000000..d90f328
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.data.repository
+
+import android.app.trust.TrustManager
+import com.android.keyguard.logging.TrustRepositoryLogger
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.shared.model.TrustModel
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+
+/** Encapsulates any state relevant to trust agents and trust grants. */
+interface TrustRepository {
+ /** Flow representing whether the current user is trusted. */
+ val isCurrentUserTrusted: Flow<Boolean>
+}
+
+@SysUISingleton
+class TrustRepositoryImpl
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val userRepository: UserRepository,
+ private val trustManager: TrustManager,
+ private val logger: TrustRepositoryLogger,
+) : TrustRepository {
+ private val latestTrustModelForUser = mutableMapOf<Int, TrustModel>()
+
+ private val trust =
+ conflatedCallbackFlow {
+ val callback =
+ object : TrustManager.TrustListener {
+ override fun onTrustChanged(
+ enabled: Boolean,
+ newlyUnlocked: Boolean,
+ userId: Int,
+ flags: Int,
+ grantMsgs: List<String>?
+ ) {
+ logger.onTrustChanged(enabled, newlyUnlocked, userId, flags, grantMsgs)
+ trySendWithFailureLogging(
+ TrustModel(enabled, userId),
+ TrustRepositoryLogger.TAG,
+ "onTrustChanged"
+ )
+ }
+
+ override fun onTrustError(message: CharSequence?) = Unit
+
+ override fun onTrustManagedChanged(enabled: Boolean, userId: Int) = Unit
+ }
+ trustManager.registerTrustListener(callback)
+ logger.trustListenerRegistered()
+ awaitClose {
+ logger.trustListenerUnregistered()
+ trustManager.unregisterTrustListener(callback)
+ }
+ }
+ .onEach {
+ latestTrustModelForUser[it.userId] = it
+ logger.trustModelEmitted(it)
+ }
+ .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
+
+ override val isCurrentUserTrusted: Flow<Boolean>
+ get() =
+ combine(trust, userRepository.selectedUserInfo, ::Pair)
+ .map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false }
+ .distinctUntilChanged()
+ .onEach { logger.isCurrentUserTrusted(it) }
+ .onStart { emit(false) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
new file mode 100644
index 0000000..6452e0e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */
+@SysUISingleton
+class AlternateBouncerInteractor
+@Inject
+constructor(
+ private val bouncerRepository: KeyguardBouncerRepository,
+ private val biometricSettingsRepository: BiometricSettingsRepository,
+ private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val systemClock: SystemClock,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ featureFlags: FeatureFlags,
+) {
+ val isModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER)
+ var legacyAlternateBouncer: LegacyAlternateBouncer? = null
+ var legacyAlternateBouncerVisibleTime: Long = NOT_VISIBLE
+
+ val isVisible: Flow<Boolean> = bouncerRepository.isAlternateBouncerVisible
+
+ /**
+ * Sets the correct bouncer states to show the alternate bouncer if it can show.
+ * @return whether alternateBouncer is visible
+ */
+ fun show(): Boolean {
+ return when {
+ isModernAlternateBouncerEnabled -> {
+ bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint())
+ isVisibleState()
+ }
+ canShowAlternateBouncerForFingerprint() -> {
+ if (legacyAlternateBouncer?.showAlternateBouncer() == true) {
+ legacyAlternateBouncerVisibleTime = systemClock.uptimeMillis()
+ true
+ } else {
+ false
+ }
+ }
+ else -> false
+ }
+ }
+
+ /**
+ * Sets the correct bouncer states to hide the bouncer. Should only be called through
+ * StatusBarKeyguardViewManager until ScrimController is refactored to use
+ * alternateBouncerInteractor.
+ * @return true if the alternate bouncer was newly hidden, else false.
+ */
+ fun hide(): Boolean {
+ return if (isModernAlternateBouncerEnabled) {
+ val wasAlternateBouncerVisible = isVisibleState()
+ bouncerRepository.setAlternateVisible(false)
+ wasAlternateBouncerVisible && !isVisibleState()
+ } else {
+ legacyAlternateBouncer?.hideAlternateBouncer() ?: false
+ }
+ }
+
+ fun isVisibleState(): Boolean {
+ return if (isModernAlternateBouncerEnabled) {
+ bouncerRepository.isAlternateBouncerVisible.value
+ } else {
+ legacyAlternateBouncer?.isShowingAlternateBouncer ?: false
+ }
+ }
+
+ fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+ bouncerRepository.setAlternateBouncerUIAvailable(isAvailable)
+ }
+
+ fun canShowAlternateBouncerForFingerprint(): Boolean {
+ return if (isModernAlternateBouncerEnabled) {
+ bouncerRepository.isAlternateBouncerUIAvailable.value &&
+ biometricSettingsRepository.isFingerprintEnrolled.value &&
+ biometricSettingsRepository.isStrongBiometricAllowed.value &&
+ biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
+ !deviceEntryFingerprintAuthRepository.isLockedOut.value
+ } else {
+ legacyAlternateBouncer != null &&
+ keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true)
+ }
+ }
+
+ /**
+ * Whether the alt bouncer has shown for a minimum time before allowing touches to dismiss the
+ * alternate bouncer and show the primary bouncer.
+ */
+ fun hasAlternateBouncerShownWithMinTime(): Boolean {
+ return if (isModernAlternateBouncerEnabled) {
+ (systemClock.uptimeMillis() - bouncerRepository.lastAlternateBouncerVisibleTime) >
+ MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS
+ } else {
+ systemClock.uptimeMillis() - legacyAlternateBouncerVisibleTime > 200
+ }
+ }
+
+ companion object {
+ private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L
+ private const val NOT_VISIBLE = -1L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index fd2d271..86f65dde 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,9 +21,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration
@@ -44,16 +45,16 @@
override fun start() {
listenForDozingToLockscreen()
+ listenForDozingToGone()
}
private fun listenForDozingToLockscreen() {
scope.launch {
- keyguardInteractor.dozeTransitionModel
+ keyguardInteractor.wakefulnessModel
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
- .collect { pair ->
- val (dozeTransitionModel, lastStartedTransition) = pair
+ .collect { (wakefulnessModel, lastStartedTransition) ->
if (
- isDozeOff(dozeTransitionModel.to) &&
+ isWakingOrStartingToWake(wakefulnessModel) &&
lastStartedTransition.to == KeyguardState.DOZING
) {
keyguardTransitionRepository.startTransition(
@@ -69,6 +70,28 @@
}
}
+ private fun listenForDozingToGone() {
+ scope.launch {
+ keyguardInteractor.biometricUnlockState
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { (biometricUnlockState, lastStartedTransition) ->
+ if (
+ lastStartedTransition.to == KeyguardState.DOZING &&
+ isWakeAndUnlock(biometricUnlockState)
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.DOZING,
+ KeyguardState.GONE,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 3b09ae7..81a5828 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -21,7 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -31,8 +31,10 @@
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@SysUISingleton
@@ -56,7 +58,7 @@
scope.launch {
// Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
// otherwise would have gone through OCCLUDED first
- keyguardInteractor.isDreamingWithOverlay
+ keyguardInteractor.isAbleToDream
.sample(
combine(
keyguardInteractor.dozeTransitionModel,
@@ -65,8 +67,7 @@
),
::toTriple
)
- .collect { triple ->
- val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
+ .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) ->
if (
!isDreaming &&
isDozeOff(dozeTransitionModel.to) &&
@@ -88,6 +89,9 @@
private fun listenForDreamingToOccluded() {
scope.launch {
keyguardInteractor.isDreaming
+ // Add a slight delay, as dreaming and occluded events will arrive with a small gap
+ // in time. This prevents a transition to OCCLUSION happening prematurely.
+ .onEach { delay(50) }
.sample(
combine(
keyguardInteractor.isKeyguardOccluded,
@@ -96,8 +100,7 @@
),
::toTriple
)
- .collect { triple ->
- val (isDreaming, isOccluded, lastStartedTransition) = triple
+ .collect { (isDreaming, isOccluded, lastStartedTransition) ->
if (
isOccluded &&
!isDreaming &&
@@ -123,24 +126,18 @@
private fun listenForDreamingToGone() {
scope.launch {
- keyguardInteractor.biometricUnlockState
- .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
- .collect { pair ->
- val (biometricUnlockState, keyguardState) = pair
- if (
- keyguardState == KeyguardState.DREAMING &&
- isWakeAndUnlock(biometricUnlockState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- KeyguardState.DREAMING,
- KeyguardState.GONE,
- getAnimator(),
- )
+ keyguardInteractor.biometricUnlockState.collect { biometricUnlockState ->
+ if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.DREAMING,
+ KeyguardState.GONE,
+ getAnimator(),
)
- }
+ )
}
+ }
}
}
@@ -151,8 +148,7 @@
keyguardTransitionInteractor.finishedKeyguardState,
::Pair
)
- .collect { pair ->
- val (dozeTransitionModel, keyguardState) = pair
+ .collect { (dozeTransitionModel, keyguardState) ->
if (
dozeTransitionModel.to == DozeStateModel.DOZE &&
keyguardState == KeyguardState.DREAMING
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 553fafe..b5bcd45 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
@@ -26,7 +26,10 @@
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
@@ -40,22 +43,23 @@
) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForGoneToAod()
+ listenForGoneToAodOrDozing()
listenForGoneToDreaming()
+ listenForGoneToLockscreen()
}
- private fun listenForGoneToDreaming() {
+ // Primarily for when the user chooses to lock down the device
+ private fun listenForGoneToLockscreen() {
scope.launch {
- keyguardInteractor.isAbleToDream
- .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
- .collect { pair ->
- val (isAbleToDream, keyguardState) = pair
- if (isAbleToDream && keyguardState == KeyguardState.GONE) {
+ keyguardInteractor.isKeyguardShowing
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { (isKeyguardShowing, lastStartedStep) ->
+ if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.GONE,
- KeyguardState.DREAMING,
+ KeyguardState.LOCKSCREEN,
getAnimator(),
)
)
@@ -64,21 +68,50 @@
}
}
- private fun listenForGoneToAod() {
+ private fun listenForGoneToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { (isAbleToDream, lastStartedStep) ->
+ if (isAbleToDream && lastStartedStep.to == KeyguardState.GONE) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.GONE,
+ KeyguardState.DREAMING,
+ getAnimator(TO_DREAMING_DURATION),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForGoneToAodOrDozing() {
scope.launch {
keyguardInteractor.wakefulnessModel
- .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
- .collect { pair ->
- val (wakefulnessState, keyguardState) = pair
+ .sample(
+ combine(
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ keyguardInteractor.isAodAvailable,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { (wakefulnessState, lastStartedStep, isAodAvailable) ->
if (
- keyguardState == KeyguardState.GONE &&
+ lastStartedStep.to == KeyguardState.GONE &&
wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.GONE,
- KeyguardState.AOD,
+ if (isAodAvailable) {
+ KeyguardState.AOD
+ } else {
+ KeyguardState.DOZING
+ },
getAnimator(),
)
)
@@ -87,14 +120,15 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 500L
+ private val DEFAULT_DURATION = 500.milliseconds
+ val TO_DREAMING_DURATION = 933.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 326acc9..5674e2a 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
@@ -21,15 +21,17 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
@@ -46,12 +48,11 @@
private val keyguardTransitionRepository: KeyguardTransitionRepository,
) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
- private var transitionId: UUID? = null
-
override fun start() {
listenForLockscreenToGone()
listenForLockscreenToOccluded()
- listenForLockscreenToAod()
+ listenForLockscreenToCamera()
+ listenForLockscreenToAodOrDozing()
listenForLockscreenToBouncer()
listenForLockscreenToDreaming()
listenForLockscreenToBouncerDragging()
@@ -69,7 +70,7 @@
name,
KeyguardState.LOCKSCREEN,
KeyguardState.DREAMING,
- getAnimator(),
+ getAnimator(TO_DREAMING_DURATION),
)
)
}
@@ -101,6 +102,7 @@
/* Starts transitions when manually dragging up the bouncer from the lockscreen. */
private fun listenForLockscreenToBouncerDragging() {
+ var transitionId: UUID? = null
scope.launch {
shadeRepository.shadeModel
.sample(
@@ -111,25 +113,43 @@
),
::toTriple
)
- .collect { triple ->
- val (shadeModel, keyguardState, statusBarState) = triple
-
+ .collect { (shadeModel, keyguardState, statusBarState) ->
val id = transitionId
if (id != null) {
// An existing `id` means a transition is started, and calls to
- // `updateTransition` will control it until FINISHED
- keyguardTransitionRepository.updateTransition(
- id,
- 1f - shadeModel.expansionAmount,
- if (
- shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
- ) {
- transitionId = null
+ // `updateTransition` will control it until FINISHED or CANCELED
+ var nextState =
+ if (shadeModel.expansionAmount == 0f) {
TransitionState.FINISHED
+ } else if (shadeModel.expansionAmount == 1f) {
+ TransitionState.CANCELED
} else {
TransitionState.RUNNING
}
+ keyguardTransitionRepository.updateTransition(
+ id,
+ 1f - shadeModel.expansionAmount,
+ nextState,
)
+
+ if (
+ nextState == TransitionState.CANCELED ||
+ nextState == TransitionState.FINISHED
+ ) {
+ transitionId = null
+ }
+
+ // If canceled, just put the state back
+ if (nextState == TransitionState.CANCELED) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.BOUNCER,
+ to = KeyguardState.LOCKSCREEN,
+ animator = getAnimator(0.milliseconds)
+ )
+ )
+ }
} else {
// TODO (b/251849525): Remove statusbarstate check when that state is
// integrated into KeyguardTransitionRepository
@@ -184,17 +204,14 @@
),
::toTriple
)
- .collect { triple ->
- val (isOccluded, keyguardState, isDreaming) = triple
- // Occlusion signals come from the framework, and should interrupt any
- // existing transition
- if (isOccluded && !isDreaming) {
+ .collect { (isOccluded, keyguardState, isDreaming) ->
+ if (isOccluded && !isDreaming && keyguardState == KeyguardState.LOCKSCREEN) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
keyguardState,
KeyguardState.OCCLUDED,
- getAnimator(),
+ getAnimator(TO_OCCLUDED_DURATION),
)
)
}
@@ -202,19 +219,59 @@
}
}
- private fun listenForLockscreenToAod() {
+ /** This signal may come in before the occlusion signal, and can provide a custom transition */
+ private fun listenForLockscreenToCamera() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ keyguardInteractor.onCameraLaunchDetected
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
- .collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ .collect { (_, lastStartedStep) ->
+ // DREAMING/AOD/OFF may trigger on the first power button push, so include this
+ // state in order to cancel and correct the transition
+ if (
+ lastStartedStep.to == KeyguardState.LOCKSCREEN ||
+ lastStartedStep.to == KeyguardState.DREAMING ||
+ lastStartedStep.to == KeyguardState.DOZING ||
+ lastStartedStep.to == KeyguardState.AOD ||
+ lastStartedStep.to == KeyguardState.OFF
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.LOCKSCREEN,
- KeyguardState.AOD,
+ KeyguardState.OCCLUDED,
+ getAnimator(TO_OCCLUDED_DURATION),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToAodOrDozing() {
+ scope.launch {
+ keyguardInteractor.wakefulnessModel
+ .sample(
+ combine(
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ keyguardInteractor.isAodAvailable,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { (wakefulnessState, lastStartedStep, isAodAvailable) ->
+ if (
+ lastStartedStep.to == KeyguardState.LOCKSCREEN &&
+ wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ if (isAodAvailable) {
+ KeyguardState.AOD
+ } else {
+ KeyguardState.DOZING
+ },
getAnimator(),
)
)
@@ -223,14 +280,16 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 500L
+ private val DEFAULT_DURATION = 500.milliseconds
+ val TO_DREAMING_DURATION = 933.milliseconds
+ val TO_OCCLUDED_DURATION = 450.milliseconds
}
}
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 8878901..2dc8fee 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
@@ -23,12 +23,14 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
@@ -44,6 +46,7 @@
override fun start() {
listenForOccludedToLockscreen()
listenForOccludedToDreaming()
+ listenForOccludedToAodOrDozing()
}
private fun listenForOccludedToDreaming() {
@@ -70,8 +73,7 @@
scope.launch {
keyguardInteractor.isKeyguardOccluded
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
- .collect { pair ->
- val (isOccluded, lastStartedKeyguardState) = pair
+ .collect { (isOccluded, lastStartedKeyguardState) ->
// Occlusion signals come from the framework, and should interrupt any
// existing transition
if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) {
@@ -88,6 +90,39 @@
}
}
+ private fun listenForOccludedToAodOrDozing() {
+ scope.launch {
+ keyguardInteractor.wakefulnessModel
+ .sample(
+ combine(
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ keyguardInteractor.isAodAvailable,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { (wakefulnessState, lastStartedStep, isAodAvailable) ->
+ if (
+ lastStartedStep.to == KeyguardState.OCCLUDED &&
+ wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.OCCLUDED,
+ if (isAodAvailable) {
+ KeyguardState.AOD
+ } else {
+ KeyguardState.DOZING
+ },
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
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 402c179..3d39da6 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
@@ -17,20 +17,30 @@
package com.android.systemui.keyguard.domain.interactor
+import android.app.StatusBarManager
import android.graphics.Point
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
-import com.android.systemui.util.kotlin.sample
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.CommandQueue.Callbacks
import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.merge
/**
@@ -41,6 +51,7 @@
@Inject
constructor(
private val repository: KeyguardRepository,
+ private val commandQueue: CommandQueue,
) {
/**
* The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -49,6 +60,8 @@
val dozeAmount: Flow<Float> = repository.linearDozeAmount
/** Whether the system is in doze mode. */
val isDozing: Flow<Boolean> = repository.isDozing
+ /** Whether Always-on Display mode is available. */
+ val isAodAvailable: Flow<Boolean> = repository.isAodAvailable
/** Doze transition information. */
val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel
/**
@@ -58,19 +71,44 @@
val isDreaming: Flow<Boolean> = repository.isDreaming
/** Whether the system is dreaming with an overlay active */
val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
+ /** Event for when the camera gesture is detected */
+ val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> = conflatedCallbackFlow {
+ val callback =
+ object : CommandQueue.Callbacks {
+ override fun onCameraLaunchGestureDetected(source: Int) {
+ trySendWithFailureLogging(
+ cameraLaunchSourceIntToModel(source),
+ TAG,
+ "updated onCameraLaunchGestureDetected"
+ )
+ }
+ }
+
+ commandQueue.addCallback(callback)
+
+ awaitClose { commandQueue.removeCallback(callback) }
+ }
/**
* 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.
+ *
+ * Allow a brief moment to prevent rapidly oscillating between true/false signals.
*/
val isAbleToDream: Flow<Boolean> =
merge(isDreaming, isDreamingWithOverlay)
- .sample(
+ .combine(
dozeTransitionModel,
{ isDreaming, dozeTransitionModel ->
isDreaming && isDozeOff(dozeTransitionModel.to)
}
)
+ .flatMapLatest { isAbleToDream ->
+ flow {
+ delay(50)
+ emit(isAbleToDream)
+ }
+ }
.distinctUntilChanged()
/** Whether the keyguard is showing or not. */
@@ -103,4 +141,26 @@
fun isKeyguardShowing(): Boolean {
return repository.isKeyguardShowing()
}
+
+ private fun cameraLaunchSourceIntToModel(value: Int): CameraLaunchSourceModel {
+ return when (value) {
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE -> CameraLaunchSourceModel.WIGGLE
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP ->
+ CameraLaunchSourceModel.POWER_DOUBLE_TAP
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER ->
+ CameraLaunchSourceModel.LIFT_TRIGGER
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE ->
+ CameraLaunchSourceModel.QUICK_AFFORDANCE
+ else -> throw IllegalArgumentException("Invalid CameraLaunchSourceModel value: $value")
+ }
+ }
+
+ /** Sets whether quick settings or quick-quick settings is visible. */
+ fun setQuickSettingsVisible(isVisible: Boolean) {
+ repository.setQuickSettingsVisible(isVisible)
+ }
+
+ companion object {
+ private const val TAG = "KeyguardInteractor"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
new file mode 100644
index 0000000..6525a13
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.shared.model.Position
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.domain.model.KeyguardSettingsPopupMenuModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.plugins.ActivityStarter
+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.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Business logic for use-cases related to the keyguard long-press feature. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class KeyguardLongPressInteractor
+@Inject
+constructor(
+ @Application unsafeContext: Context,
+ @Application scope: CoroutineScope,
+ transitionInteractor: KeyguardTransitionInteractor,
+ repository: KeyguardRepository,
+ private val activityStarter: ActivityStarter,
+ private val logger: UiEventLogger,
+ private val featureFlags: FeatureFlags,
+ broadcastDispatcher: BroadcastDispatcher,
+) {
+ private val appContext = unsafeContext.applicationContext
+
+ private val _isLongPressHandlingEnabled: StateFlow<Boolean> =
+ if (isFeatureEnabled()) {
+ combine(
+ transitionInteractor.finishedKeyguardState.map {
+ it == KeyguardState.LOCKSCREEN
+ },
+ repository.isQuickSettingsVisible,
+ ) { isFullyTransitionedToLockScreen, isQuickSettingsVisible ->
+ isFullyTransitionedToLockScreen && !isQuickSettingsVisible
+ }
+ } else {
+ flowOf(false)
+ }
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
+ /** Whether the long-press handling feature should be enabled. */
+ val isLongPressHandlingEnabled: Flow<Boolean> = _isLongPressHandlingEnabled
+
+ private val _menu = MutableStateFlow<KeyguardSettingsPopupMenuModel?>(null)
+ /** Model for a menu that should be shown; `null` when no menu should be shown. */
+ val menu: Flow<KeyguardSettingsPopupMenuModel?> =
+ isLongPressHandlingEnabled.flatMapLatest { isEnabled ->
+ if (isEnabled) {
+ _menu
+ } else {
+ flowOf(null)
+ }
+ }
+
+ init {
+ if (isFeatureEnabled()) {
+ broadcastDispatcher
+ .broadcastFlow(
+ IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+ )
+ .onEach { hideMenu() }
+ .launchIn(scope)
+ }
+ }
+
+ /** Notifies that the user has long-pressed on the lock screen. */
+ fun onLongPress(x: Int, y: Int) {
+ if (!_isLongPressHandlingEnabled.value) {
+ return
+ }
+
+ showMenu(
+ x = x,
+ y = y,
+ )
+ }
+
+ private fun isFeatureEnabled(): Boolean {
+ return featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED) &&
+ featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI)
+ }
+
+ /** Updates application state to ask to show the menu at the given coordinates. */
+ private fun showMenu(
+ x: Int,
+ y: Int,
+ ) {
+ _menu.value =
+ KeyguardSettingsPopupMenuModel(
+ position =
+ Position(
+ x = x,
+ y = y,
+ ),
+ onClicked = {
+ hideMenu()
+ navigateToLockScreenSettings()
+ },
+ onDismissed = { hideMenu() },
+ )
+ logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
+ }
+
+ /** Updates application state to ask to hide the menu. */
+ private fun hideMenu() {
+ _menu.value = null
+ }
+
+ /** Opens the wallpaper picker screen after the device is unlocked by the user. */
+ private fun navigateToLockScreenSettings() {
+ logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
+ activityStarter.dismissKeyguardThenExecute(
+ /* action= */ {
+ appContext.startActivity(
+ Intent(Intent.ACTION_SET_WALLPAPER).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ appContext
+ .getString(R.string.config_wallpaperPickerPackage)
+ .takeIf { it.isNotEmpty() }
+ ?.let { packageName -> setPackage(packageName) }
+ }
+ )
+ true
+ },
+ /* cancel= */ {},
+ /* afterKeyguardGone= */ true,
+ )
+ }
+
+ enum class LogEvents(
+ private val _id: Int,
+ ) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "The lock screen was long-pressed and we showed the settings popup menu.")
+ LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN(1292),
+ @UiEvent(doc = "The lock screen long-press popup menu was clicked.")
+ LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED(1293),
+ ;
+
+ override fun getId() = _id
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index c219380..57c3b31 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -154,7 +154,7 @@
val slots = repository.get().getSlotPickerRepresentations()
val slot = slots.find { it.id == slotId } ?: return false
val selections =
- repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+ repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
val alreadySelected = selections.remove(affordanceId)
if (!alreadySelected) {
while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
@@ -193,7 +193,7 @@
if (affordanceId.isNullOrEmpty()) {
return if (
- repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty()
+ repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty()
) {
false
} else {
@@ -203,7 +203,7 @@
}
val selections =
- repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+ repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).toMutableList()
return if (selections.remove(affordanceId)) {
repository
.get()
@@ -220,7 +220,7 @@
/** Returns affordance IDs indexed by slot ID, for all known slots. */
suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
val slots = repository.get().getSlotPickerRepresentations()
- val selections = repository.get().getSelections()
+ val selections = repository.get().getCurrentSelections()
val affordanceById =
getAffordancePickerRepresentations().associateBy { affordance -> affordance.id }
return slots.associate { slot ->
@@ -363,6 +363,10 @@
name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED,
value = featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
),
+ KeyguardPickerFlag(
+ name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW,
+ value = featureFlags.isEnabled(Flags.WALLPAPER_FULLSCREEN_PREVIEW),
+ ),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index a2661d7..d4e23499 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -19,11 +19,14 @@
import com.android.keyguard.logging.KeyguardLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
+private val TAG = KeyguardTransitionAuditLogger::class.simpleName!!
+
/** Collect flows of interest for auditing keyguard transitions. */
@SysUISingleton
class KeyguardTransitionAuditLogger
@@ -37,35 +40,47 @@
fun start() {
scope.launch {
- keyguardInteractor.wakefulnessModel.collect { logger.v("WakefulnessModel", it) }
+ keyguardInteractor.wakefulnessModel.collect {
+ logger.log(TAG, VERBOSE, "WakefulnessModel", it)
+ }
}
scope.launch {
- keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) }
+ keyguardInteractor.isBouncerShowing.collect {
+ logger.log(TAG, VERBOSE, "Bouncer showing", it)
+ }
}
- scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } }
+ scope.launch {
+ keyguardInteractor.isDozing.collect { logger.log(TAG, VERBOSE, "isDozing", it) }
+ }
- scope.launch { keyguardInteractor.isDreaming.collect { logger.v("isDreaming", it) } }
+ scope.launch {
+ keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) }
+ }
scope.launch {
interactor.finishedKeyguardTransitionStep.collect {
- logger.i("Finished transition", it)
+ logger.log(TAG, VERBOSE, "Finished transition", it)
}
}
scope.launch {
interactor.canceledKeyguardTransitionStep.collect {
- logger.i("Canceled transition", it)
+ logger.log(TAG, VERBOSE, "Canceled transition", it)
}
}
scope.launch {
- interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) }
+ interactor.startedKeyguardTransitionStep.collect {
+ logger.log(TAG, VERBOSE, "Started transition", it)
+ }
}
scope.launch {
- keyguardInteractor.dozeTransitionModel.collect { logger.i("Doze transition", it) }
+ keyguardInteractor.dozeTransitionModel.collect {
+ logger.log(TAG, VERBOSE, "Doze transition", it)
+ }
}
}
}
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 04024be..53c80f6 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
@@ -19,17 +19,17 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
-import kotlin.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@@ -42,24 +42,39 @@
constructor(
repository: KeyguardTransitionRepository,
) {
+ /** (any)->AOD transition information */
+ val anyStateToAodTransition: Flow<TransitionStep> =
+ repository.transitions.filter { step -> step.to == KeyguardState.AOD }
+
/** AOD->LOCKSCREEN transition information. */
val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
- /** LOCKSCREEN->AOD transition information. */
- val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
-
/** DREAMING->LOCKSCREEN transition information. */
val dreamingToLockscreenTransition: Flow<TransitionStep> =
repository.transition(DREAMING, LOCKSCREEN)
+ /** GONE->DREAMING transition information. */
+ val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING)
+
+ /** LOCKSCREEN->AOD transition information. */
+ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+ /** LOCKSCREEN->BOUNCER transition information. */
+ val lockscreenToBouncerTransition: Flow<TransitionStep> =
+ repository.transition(LOCKSCREEN, BOUNCER)
+
+ /** LOCKSCREEN->DREAMING transition information. */
+ val lockscreenToDreamingTransition: Flow<TransitionStep> =
+ repository.transition(LOCKSCREEN, DREAMING)
+
+ /** LOCKSCREEN->OCCLUDED transition information. */
+ val lockscreenToOccludedTransition: Flow<TransitionStep> =
+ repository.transition(LOCKSCREEN, OCCLUDED)
+
/** OCCLUDED->LOCKSCREEN transition information. */
val occludedToLockscreenTransition: Flow<TransitionStep> =
repository.transition(OCCLUDED, LOCKSCREEN)
- /** (any)->AOD transition information */
- val anyStateToAodTransition: Flow<TransitionStep> =
- repository.transitions.filter { step -> step.to == KeyguardState.AOD }
-
/**
* AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
* Lockscreen (0f).
@@ -85,28 +100,4 @@
/* The last completed [KeyguardState] transition */
val finishedKeyguardState: Flow<KeyguardState> =
finishedKeyguardTransitionStep.map { step -> step.to }
-
- /**
- * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
- * range of [0, 1]. View animations should begin and end within a subset of this range. This
- * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
- */
- fun transitionStepAnimation(
- flow: Flow<TransitionStep>,
- params: AnimationParams,
- totalDuration: Duration,
- ): Flow<Float> {
- val start = (params.startTime / totalDuration).toFloat()
- val chunks = (totalDuration / params.duration).toFloat()
- return flow
- // When starting, emit a value of 0f to give animations a chance to set initial state
- .map { step ->
- if (step.transitionState == STARTED) {
- 0f
- } else {
- (step.value - start) * chunks
- }
- }
- .filter { value -> value >= 0f && value <= 1f }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
index c5e49c6..3099a49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
@@ -18,27 +18,29 @@
import android.view.View
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
import com.android.systemui.util.ListenerSet
import javax.inject.Inject
/** Interactor to add and remove callbacks for the bouncer. */
@SysUISingleton
class PrimaryBouncerCallbackInteractor @Inject constructor() {
- private var resetCallbacks = ListenerSet<KeyguardBouncer.KeyguardResetCallback>()
- private var expansionCallbacks = ArrayList<KeyguardBouncer.PrimaryBouncerExpansionCallback>()
+ private var resetCallbacks = ListenerSet<KeyguardResetCallback>()
+ private var expansionCallbacks = ArrayList<PrimaryBouncerExpansionCallback>()
+
/** Add a KeyguardResetCallback. */
- fun addKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+ fun addKeyguardResetCallback(callback: KeyguardResetCallback) {
resetCallbacks.addIfAbsent(callback)
}
/** Remove a KeyguardResetCallback. */
- fun removeKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+ fun removeKeyguardResetCallback(callback: KeyguardResetCallback) {
resetCallbacks.remove(callback)
}
/** Adds a callback to listen to bouncer expansion updates. */
- fun addBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) {
+ fun addBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
if (!expansionCallbacks.contains(callback)) {
expansionCallbacks.add(callback)
}
@@ -48,7 +50,7 @@
* Removes a previously added callback. If the callback was never added, this method does
* nothing.
*/
- fun removeBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) {
+ fun removeBouncerExpansionCallback(callback: PrimaryBouncerExpansionCallback) {
expansionCallbacks.remove(callback)
}
@@ -99,4 +101,40 @@
callback.onKeyguardReset()
}
}
+
+ /** Callback updated when the primary bouncer's show and hide states change. */
+ interface PrimaryBouncerExpansionCallback {
+ /**
+ * Invoked when the bouncer expansion reaches [EXPANSION_VISIBLE]. This is NOT called each
+ * time the bouncer is shown, but rather only when the fully shown amount has changed based
+ * on the panel expansion. The bouncer's visibility can still change when the expansion
+ * amount hasn't changed. See [PrimaryBouncerInteractor.isFullyShowing] for the checks for
+ * the bouncer showing state.
+ */
+ fun onFullyShown() {}
+
+ /** Invoked when the bouncer is starting to transition to a hidden state. */
+ fun onStartingToHide() {}
+
+ /** Invoked when the bouncer is starting to transition to a visible state. */
+ fun onStartingToShow() {}
+
+ /** Invoked when the bouncer expansion reaches [EXPANSION_HIDDEN]. */
+ fun onFullyHidden() {}
+
+ /**
+ * From 0f [EXPANSION_VISIBLE] when fully visible to 1f [EXPANSION_HIDDEN] when fully hidden
+ */
+ fun onExpansionChanged(bouncerHideAmount: Float) {}
+
+ /**
+ * Invoked when visibility of KeyguardBouncer has changed. Note the bouncer expansion can be
+ * [EXPANSION_VISIBLE], but the view's visibility can be [View.INVISIBLE].
+ */
+ fun onVisibilityChanged(isVisible: Boolean) {}
+ }
+
+ interface KeyguardResetCallback {
+ fun onKeyguardReset()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index 2cf5fb9..96bf815 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -32,11 +32,11 @@
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.statusbar.phone.KeyguardBouncer
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
@@ -113,6 +113,8 @@
0f
}
}
+ /** Allow for interaction when just about fully visible */
+ val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 }
// TODO(b/243685699): Move isScrimmed logic to data layer.
// TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
@@ -143,7 +145,7 @@
Trace.beginSection("KeyguardBouncer#show")
repository.setPrimaryScrimmed(isScrimmed)
if (isScrimmed) {
- setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+ setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
}
if (resumeBouncer) {
@@ -204,14 +206,14 @@
}
if (
- expansion == KeyguardBouncer.EXPANSION_VISIBLE &&
- oldExpansion != KeyguardBouncer.EXPANSION_VISIBLE
+ expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+ oldExpansion != KeyguardBouncerConstants.EXPANSION_VISIBLE
) {
falsingCollector.onBouncerShown()
primaryBouncerCallbackInteractor.dispatchFullyShown()
} else if (
- expansion == KeyguardBouncer.EXPANSION_HIDDEN &&
- oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN
+ expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN &&
+ oldExpansion != KeyguardBouncerConstants.EXPANSION_HIDDEN
) {
/*
* There are cases where #hide() was not invoked, such as when
@@ -222,8 +224,8 @@
DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
primaryBouncerCallbackInteractor.dispatchFullyHidden()
} else if (
- expansion != KeyguardBouncer.EXPANSION_VISIBLE &&
- oldExpansion == KeyguardBouncer.EXPANSION_VISIBLE
+ expansion != KeyguardBouncerConstants.EXPANSION_VISIBLE &&
+ oldExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE
) {
primaryBouncerCallbackInteractor.dispatchStartingToHide()
repository.setPrimaryStartingToHide(true)
@@ -303,7 +305,7 @@
fun isFullyShowing(): Boolean {
return (repository.primaryBouncerShowingSoon.value ||
repository.primaryBouncerVisible.value) &&
- repository.panelExpansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
+ repository.panelExpansionAmount.value == KeyguardBouncerConstants.EXPANSION_VISIBLE &&
repository.primaryBouncerStartingDisappearAnimation.value == null
}
@@ -315,8 +317,8 @@
/** If bouncer expansion is between 0f and 1f non-inclusive. */
fun isInTransit(): Boolean {
return repository.primaryBouncerShowingSoon.value ||
- repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN &&
- repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE
+ repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_HIDDEN &&
+ repository.panelExpansionAmount.value != KeyguardBouncerConstants.EXPANSION_VISIBLE
}
/** Return whether bouncer is animating away. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt
new file mode 100644
index 0000000..7c61e71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.domain.model
+
+import com.android.systemui.common.shared.model.Position
+
+/** Models a settings popup menu for the lock screen. */
+data class KeyguardSettingsPopupMenuModel(
+ /** Where the menu should be anchored, roughly in screen space. */
+ val position: Position,
+ /** Callback to invoke when the menu gets clicked by the user. */
+ val onClicked: () -> Unit,
+ /** Callback to invoke when the menu gets dismissed by the user. */
+ val onDismissed: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt
new file mode 100644
index 0000000..8222dd5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.shared.constants
+
+object KeyguardBouncerConstants {
+ /**
+ * Values for the bouncer expansion represented as the panel expansion. Panel expansion 1f =
+ * panel fully showing = bouncer fully hidden Panel expansion 0f = panel fully hiding = bouncer
+ * fully showing
+ */
+ const val EXPANSION_HIDDEN = 1f
+ const val EXPANSION_VISIBLE = 0f
+ const val ALPHA_EXPANSION_THRESHOLD = 0.95f
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt
similarity index 63%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt
index 67733e9..19baf77 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/CameraLaunchSourceModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -15,11 +15,14 @@
*/
package com.android.systemui.keyguard.shared.model
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
-)
+/** Camera launch sources */
+enum class CameraLaunchSourceModel {
+ /** Device is wiggled */
+ WIGGLE,
+ /** Power button has been double tapped */
+ POWER_DOUBLE_TAP,
+ /** Device has been lifted */
+ LIFT_TRIGGER,
+ /** Quick affordance button has been pressed */
+ QUICK_AFFORDANCE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
index 7d13359..e7e9159 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.shared.model
+import android.content.Intent
import androidx.annotation.DrawableRes
/**
@@ -45,4 +46,7 @@
* user to a destination where they can re-enable it.
*/
val actionComponentName: String? = null,
+
+ /** Optional [Intent] to use to start an activity to configure this affordance. */
+ val configureIntent: Intent? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt
index 67733e9..4fd14b1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TrustModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
* 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
+ * limitations under the License.
*/
+
package com.android.systemui.keyguard.shared.model
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
+/** Represents the trust state */
+data class TrustModel(
+ /** If true, the system believes the environment to be trusted. */
+ val isTrusted: Boolean,
+ /** The user, for which the trust changed. */
+ val userId: Int,
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
new file mode 100644
index 0000000..ca1e27c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -0,0 +1,106 @@
+/*
+ * 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
+
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/**
+ * For the given transition params, construct a flow using [createFlow] for the specified portion of
+ * the overall transition.
+ */
+class KeyguardTransitionAnimationFlow(
+ private val transitionDuration: Duration,
+ private val transitionFlow: Flow<TransitionStep>,
+) {
+ /**
+ * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in
+ * the range of [0, 1]. View animations should begin and end within a subset of this range. This
+ * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+ */
+ fun createFlow(
+ duration: Duration,
+ onStep: (Float) -> Float,
+ startTime: Duration = 0.milliseconds,
+ onCancel: (() -> Float)? = null,
+ onFinish: (() -> Float)? = null,
+ interpolator: Interpolator = LINEAR,
+ ): Flow<Float> {
+ if (!duration.isPositive()) {
+ throw IllegalArgumentException("duration must be a positive number: $duration")
+ }
+ if ((startTime + duration).compareTo(transitionDuration) > 0) {
+ throw IllegalArgumentException(
+ "startTime($startTime) + duration($duration) must be" +
+ " <= transitionDuration($transitionDuration)"
+ )
+ }
+
+ val start = (startTime / transitionDuration).toFloat()
+ val chunks = (transitionDuration / duration).toFloat()
+ var isComplete = true
+
+ fun stepToValue(step: TransitionStep): Float? {
+ val value = (step.value - start) * chunks
+ return when (step.transitionState) {
+ // When starting, make sure to always emit. If a transition is started from the
+ // middle, it is possible this animation is being skipped but we need to inform
+ // the ViewModels of the last update
+ STARTED -> {
+ isComplete = false
+ max(0f, min(1f, value))
+ }
+ // Always send a final value of 1. Because of rounding, [value] may never be
+ // exactly 1.
+ RUNNING ->
+ if (isComplete) {
+ null
+ } else if (value >= 1f) {
+ isComplete = true
+ 1f
+ } else if (value >= 0f) {
+ value
+ } else {
+ null
+ }
+ else -> null
+ }?.let { onStep(interpolator.getInterpolation(it)) }
+ }
+
+ return transitionFlow
+ .map { step ->
+ when (step.transitionState) {
+ STARTED -> stepToValue(step)
+ RUNNING -> stepToValue(step)
+ CANCELED -> onCancel?.invoke()
+ FINISHED -> onFinish?.invoke()
+ }
+ }
+ .filterNotNull()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 0e4058b..3319f9d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -45,11 +45,11 @@
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.kotlin.pairwise
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
@@ -67,6 +67,8 @@
object KeyguardBottomAreaViewBinder {
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
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
@@ -129,18 +131,6 @@
}
launch {
- viewModel.startButton
- .map { it.isActivated }
- .pairwise()
- .collect { (prev, next) ->
- when {
- !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
- prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
- }
- }
- }
-
- launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
view = endButton,
@@ -153,18 +143,6 @@
}
launch {
- viewModel.endButton
- .map { it.isActivated }
- .pairwise()
- .collect { (prev, next) ->
- when {
- !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
- prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
- }
- }
- }
-
- launch {
viewModel.isOverlayContainerVisible.collect { isVisible ->
overlayContainer.visibility =
if (isVisible) {
@@ -186,12 +164,26 @@
ambientIndicationArea?.alpha = alpha
indicationArea.alpha = alpha
- startButton.alpha = alpha
- endButton.alpha = alpha
}
}
launch {
+ updateButtonAlpha(
+ view = startButton,
+ viewModel = viewModel.startButton,
+ alphaFlow = viewModel.alpha,
+ )
+ }
+
+ launch {
+ updateButtonAlpha(
+ view = endButton,
+ viewModel = viewModel.endButton,
+ alphaFlow = viewModel.alpha,
+ )
+ }
+
+ launch {
viewModel.indicationAreaTranslationX.collect { translationX ->
indicationArea.translationX = translationX
ambientIndicationArea?.translationX = translationX
@@ -340,6 +332,11 @@
} else {
null
}
+ view
+ .animate()
+ .scaleX(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
+ .scaleY(if (viewModel.isSelected) SCALE_SELECTED_BUTTON else 1f)
+ .start()
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
@@ -358,6 +355,17 @@
view.isSelected = viewModel.isSelected
}
+ private suspend fun updateButtonAlpha(
+ view: View,
+ viewModel: Flow<KeyguardQuickAffordanceViewModel>,
+ alphaFlow: Flow<Float>,
+ ) {
+ combine(viewModel.map { it.isDimmed }, alphaFlow) { isDimmed, alpha ->
+ if (isDimmed) DIM_ALPHA else alpha
+ }
+ .collect { view.alpha = it }
+ }
+
private class OnTouchListener(
private val view: View,
private val viewModel: KeyguardQuickAffordanceViewModel,
@@ -367,14 +375,12 @@
private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
private var longPressAnimator: ViewPropertyAnimator? = null
- private var downTimestamp = 0L
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return when (event?.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
- downTimestamp = System.currentTimeMillis()
longPressAnimator =
view
.animate()
@@ -383,6 +389,13 @@
.setDuration(longPressDurationMs)
.withEndAction {
view.setOnClickListener {
+ vibratorHelper?.vibrate(
+ if (viewModel.isActivated) {
+ Vibrations.Activated
+ } else {
+ Vibrations.Deactivated
+ }
+ )
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = viewModel.configKey,
@@ -414,7 +427,7 @@
MotionEvent.ACTION_UP -> {
cancel(
onAnimationEnd =
- if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
+ if (event.eventTime - event.downTime < longPressDurationMs) {
Runnable {
messageDisplayer.invoke(
R.string.keyguard_affordance_press_too_short
@@ -455,7 +468,6 @@
}
private fun cancel(onAnimationEnd: Runnable? = null) {
- downTimestamp = 0L
longPressAnimator?.cancel()
longPressAnimator = null
view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index f772b17..9f09d53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -19,6 +19,7 @@
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
+import android.window.OnBackAnimationCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.internal.policy.SystemBarUtils
@@ -27,10 +28,10 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.dagger.KeyguardBouncerComponent
import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@@ -55,6 +56,10 @@
mode == KeyguardSecurityModel.SecurityMode.SimPuk
}
+ override fun getBackCallback(): OnBackAnimationCallback {
+ return hostViewController.backCallback
+ }
+
override fun shouldDismissOnMenuPressed(): Boolean {
return hostViewController.shouldEnableMenuKey()
}
@@ -101,13 +106,6 @@
hostViewController.appear(
SystemBarUtils.getStatusBarHeight(view.context)
)
- }
- }
-
- launch {
- viewModel.showWithFullExpansion.collect { model ->
- hostViewController.resetSecurityContainer()
- hostViewController.showPromptReason(model.promptReason)
hostViewController.onResume()
}
}
@@ -156,6 +154,12 @@
}
launch {
+ viewModel.isInteractable.collect { isInteractable ->
+ hostViewController.setInteractable(isInteractable)
+ }
+ }
+
+ launch {
viewModel.isBouncerVisible
.filter { !it }
.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt
new file mode 100644
index 0000000..d85682b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.binder
+
+import android.annotation.SuppressLint
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import android.widget.PopupWindow
+import com.android.systemui.R
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.common.ui.binder.TextViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsPopupMenuViewModel
+
+object KeyguardLongPressPopupViewBinder {
+ @SuppressLint("InflateParams") // We don't care that the parent is null.
+ fun createAndShow(
+ container: View,
+ viewModel: KeyguardSettingsPopupMenuViewModel,
+ onDismissed: () -> Unit,
+ ): () -> Unit {
+ val contentView: View =
+ LayoutInflater.from(container.context)
+ .inflate(
+ R.layout.keyguard_settings_popup_menu,
+ null,
+ )
+
+ contentView.setOnClickListener { viewModel.onClicked() }
+ IconViewBinder.bind(
+ icon = viewModel.icon,
+ view = contentView.requireViewById(R.id.icon),
+ )
+ TextViewBinder.bind(
+ view = contentView.requireViewById(R.id.text),
+ viewModel = viewModel.text,
+ )
+
+ val popupWindow =
+ PopupWindow(container.context).apply {
+ windowLayoutType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
+ setBackgroundDrawable(null)
+ animationStyle = com.android.internal.R.style.Animation_Dialog
+ isOutsideTouchable = true
+ isFocusable = true
+ setContentView(contentView)
+ setOnDismissListener { onDismissed() }
+ contentView.measure(
+ View.MeasureSpec.makeMeasureSpec(
+ 0,
+ View.MeasureSpec.UNSPECIFIED,
+ ),
+ View.MeasureSpec.makeMeasureSpec(
+ 0,
+ View.MeasureSpec.UNSPECIFIED,
+ ),
+ )
+ showAtLocation(
+ container,
+ Gravity.NO_GRAVITY,
+ viewModel.position.x - contentView.measuredWidth / 2,
+ viewModel.position.y -
+ contentView.measuredHeight -
+ container.context.resources.getDimensionPixelSize(
+ R.dimen.keyguard_long_press_settings_popup_vertical_offset
+ ),
+ )
+ }
+
+ return { popupWindow.dismiss() }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
new file mode 100644
index 0000000..ef3f242
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.binder
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.view.LongPressHandlingView
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.FalsingManager
+import kotlinx.coroutines.launch
+
+object KeyguardLongPressViewBinder {
+ /**
+ * Drives UI for the lock screen long-press feature.
+ *
+ * @param view The view that listens for long-presses.
+ * @param viewModel The view-model that models the UI state.
+ * @param onSingleTap A callback to invoke when the system decides that there was a single tap.
+ * @param falsingManager [FalsingManager] for making sure the long-press didn't just happen in
+ * the user's pocket.
+ */
+ @JvmStatic
+ fun bind(
+ view: LongPressHandlingView,
+ viewModel: KeyguardLongPressViewModel,
+ onSingleTap: () -> Unit,
+ falsingManager: FalsingManager,
+ ) {
+ view.listener =
+ object : LongPressHandlingView.Listener {
+ override fun onLongPressDetected(view: View, x: Int, y: Int) {
+ if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+ return
+ }
+
+ viewModel.onLongPress(
+ x = x,
+ y = y,
+ )
+ }
+
+ override fun onSingleTapDetected(view: View) {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+ return
+ }
+
+ onSingleTap()
+ }
+ }
+
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.isLongPressHandlingEnabled.collect { isEnabled ->
+ view.setLongPressHandlingEnabled(isEnabled)
+ }
+ }
+
+ launch {
+ var dismissMenu: (() -> Unit)? = null
+
+ viewModel.menu.collect { menuOrNull ->
+ if (menuOrNull != null) {
+ dismissMenu =
+ KeyguardLongPressPopupViewBinder.createAndShow(
+ container = view,
+ viewModel = menuOrNull,
+ onDismissed = menuOrNull.onDismissed,
+ )
+ } else {
+ dismissMenu?.invoke()
+ }
+ }
+ }
+ }
+ }
+ }
+}
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 a5ae8ba5..8808574 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
@@ -24,7 +24,6 @@
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.os.IBinder
-import android.view.Gravity
import android.view.LayoutInflater
import android.view.SurfaceControlViewHost
import android.view.View
@@ -65,6 +64,11 @@
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
private val height: Int = bundle.getInt(KEY_VIEW_HEIGHT)
+ private val shouldHighlightSelectedAffordance: Boolean =
+ bundle.getBoolean(
+ KeyguardQuickAffordancePreviewConstants.KEY_HIGHLIGHT_QUICK_AFFORDANCES,
+ false,
+ )
private var host: SurfaceControlViewHost
@@ -82,6 +86,7 @@
bundle.getString(
KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
),
+ shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
)
runBlocking(mainDispatcher) {
host =
@@ -154,8 +159,7 @@
bottomAreaView,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.WRAP_CONTENT,
- Gravity.BOTTOM,
+ FrameLayout.LayoutParams.MATCH_PARENT,
),
)
}
@@ -195,7 +199,13 @@
?.events
?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
clockView?.let { parentView.removeView(it) }
- clockView = clockController.clock?.largeClock?.view?.apply { parentView.addView(this) }
+ clockView =
+ clockController.clock?.largeClock?.view?.apply {
+ if (shouldHighlightSelectedAffordance) {
+ alpha = DIM_ALPHA
+ }
+ parentView.addView(this)
+ }
}
companion object {
@@ -203,5 +213,7 @@
private const val KEY_VIEW_WIDTH = "width"
private const val KEY_VIEW_HEIGHT = "height"
private const val KEY_DISPLAY_ID = "display_id"
+
+ private const val DIM_ALPHA = 0.3f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index e164f5d..8d6545a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -21,11 +21,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
/**
* Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -37,33 +36,46 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_LOCKSCREEN_DURATION,
+ transitionFlow = interactor.dreamingToLockscreenTransition,
+ )
/** Dream overlay y-translation on exit */
fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
- EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
- }
+ return transitionAnimation.createFlow(
+ duration = 600.milliseconds,
+ onStep = { it * translatePx },
+ interpolator = EMPHASIZED_ACCELERATE,
+ )
}
/** Dream overlay views alpha - fade out */
- val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+ val dreamOverlayAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- }
+ return transitionAnimation.createFlow(
+ duration = TO_LOCKSCREEN_DURATION,
+ onStep = { value -> -translatePx + value * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_DECELERATE,
+ )
}
/** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.dreamingToLockscreenTransition,
- params,
- totalDuration = TO_LOCKSCREEN_DURATION
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ startTime = 233.milliseconds,
+ duration = 250.milliseconds,
+ onStep = { it },
)
- }
companion object {
/* Length of time before ending the dream activity, in order to start unoccluding */
@@ -71,11 +83,5 @@
@JvmField
val LOCKSCREEN_ANIMATION_DURATION_MS =
(TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
-
- val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
- val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
- val LOCKSCREEN_ALPHA =
- AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
new file mode 100644
index 0000000..f16827d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */
+@SysUISingleton
+class GoneToDreamingTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_DREAMING_DURATION,
+ transitionFlow = interactor.goneToDreamingTransition,
+ )
+
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return transitionAnimation.createFlow(
+ duration = 500.milliseconds,
+ onStep = { it * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
+ )
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 5d85680..1e3b60c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -45,12 +45,17 @@
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {
+ data class PreviewMode(
+ val isInPreviewMode: Boolean = false,
+ val shouldHighlightSelectedAffordance: Boolean = false,
+ )
+
/**
* Whether this view-model instance is powering the preview experience that renders exclusively
* in the wallpaper picker application. This should _always_ be `false` for the real lock screen
* experience.
*/
- private val isInPreviewMode = MutableStateFlow(false)
+ private val previewMode = MutableStateFlow(PreviewMode())
/**
* ID of the slot that's currently selected in the preview that renders exclusively in the
@@ -87,8 +92,8 @@
keyguardInteractor.isDozing.map { !it }.distinctUntilChanged()
/** An observable for the alpha level for the entire bottom area. */
val alpha: Flow<Float> =
- isInPreviewMode.flatMapLatest { isInPreviewMode ->
- if (isInPreviewMode) {
+ previewMode.flatMapLatest {
+ if (it.isInPreviewMode) {
flowOf(1f)
} else {
bottomAreaInteractor.alpha.distinctUntilChanged()
@@ -129,9 +134,18 @@
* lock screen.
*
* @param initiallySelectedSlotId The ID of the initial slot to render as the selected one.
+ * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be
+ * highlighted (while all others are dimmed to make the selected one stand out).
*/
- fun enablePreviewMode(initiallySelectedSlotId: String?) {
- isInPreviewMode.value = true
+ fun enablePreviewMode(
+ initiallySelectedSlotId: String?,
+ shouldHighlightSelectedAffordance: Boolean,
+ ) {
+ previewMode.value =
+ PreviewMode(
+ isInPreviewMode = true,
+ shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
+ )
onPreviewSlotSelected(
initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
)
@@ -150,9 +164,9 @@
private fun button(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceViewModel> {
- return isInPreviewMode.flatMapLatest { isInPreviewMode ->
+ return previewMode.flatMapLatest { previewMode ->
combine(
- if (isInPreviewMode) {
+ if (previewMode.isInPreviewMode) {
quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
} else {
quickAffordanceInteractor.quickAffordance(position = position)
@@ -161,11 +175,18 @@
areQuickAffordancesFullyOpaque,
selectedPreviewSlotId,
) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId ->
+ val isSelected = selectedPreviewSlotId == position.toSlotId()
model.toViewModel(
- animateReveal = !isInPreviewMode && animateReveal,
- isClickable = isFullyOpaque && !isInPreviewMode,
+ animateReveal = !previewMode.isInPreviewMode && animateReveal,
+ isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
isSelected =
- (isInPreviewMode && selectedPreviewSlotId == position.toSlotId()),
+ previewMode.isInPreviewMode &&
+ previewMode.shouldHighlightSelectedAffordance &&
+ isSelected,
+ isDimmed =
+ previewMode.isInPreviewMode &&
+ previewMode.shouldHighlightSelectedAffordance &&
+ !isSelected,
)
}
.distinctUntilChanged()
@@ -176,6 +197,7 @@
animateReveal: Boolean,
isClickable: Boolean,
isSelected: Boolean,
+ isDimmed: Boolean,
): KeyguardQuickAffordanceViewModel {
return when (this) {
is KeyguardQuickAffordanceModel.Visible ->
@@ -194,6 +216,7 @@
isActivated = activationState is ActivationState.Active,
isSelected = isSelected,
useLongPress = quickAffordanceInteractor.useLongPress,
+ isDimmed = isDimmed,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index e5d4e49..b8b3a8e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -22,10 +22,8 @@
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
/** Models UI state for the lock screen bouncer; handles user input. */
@@ -41,13 +39,12 @@
/** Observe on bouncer visibility. */
val isBouncerVisible: Flow<Boolean> = interactor.isVisible
+ /** Can the user interact with the view? */
+ val isInteractable: Flow<Boolean> = interactor.isInteractable
+
/** Observe whether bouncer is showing. */
val show: Flow<KeyguardBouncerModel> = interactor.show
- /** Observe visible expansion when bouncer is showing. */
- val showWithFullExpansion: Flow<KeyguardBouncerModel> =
- interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
-
/** Observe whether bouncer is hiding. */
val hide: Flow<Unit> = interactor.hide
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
new file mode 100644
index 0000000..d896390
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Models UI state to support the lock screen long-press feature. */
+class KeyguardLongPressViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardLongPressInteractor,
+) {
+
+ /** Whether the long-press handling feature should be enabled. */
+ val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
+
+ /** View-model for a menu that should be shown; `null` when no menu should be shown. */
+ val menu: Flow<KeyguardSettingsPopupMenuViewModel?> =
+ interactor.menu.map { model ->
+ model?.let {
+ KeyguardSettingsPopupMenuViewModel(
+ icon =
+ Icon.Resource(
+ res = R.drawable.ic_settings,
+ contentDescription = null,
+ ),
+ text =
+ Text.Resource(
+ res = R.string.lock_screen_settings,
+ ),
+ position = model.position,
+ onClicked = model.onClicked,
+ onDismissed = model.onDismissed,
+ )
+ }
+ }
+
+ /** Notifies that the user has long-pressed on the lock screen. */
+ fun onLongPress(
+ x: Int,
+ y: Int,
+ ) {
+ interactor.onLongPress(
+ x = x,
+ y = y,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index cf3a6da..cb68a82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -31,6 +31,7 @@
val isActivated: Boolean = false,
val isSelected: Boolean = false,
val useLongPress: Boolean = false,
+ val isDimmed: Boolean = false,
) {
data class OnClickedParameters(
val configKey: String,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt
new file mode 100644
index 0000000..0571b05
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Position
+import com.android.systemui.common.shared.model.Text
+
+/** Models the UI state of a keyguard settings popup menu. */
+data class KeyguardSettingsPopupMenuViewModel(
+ val icon: Icon,
+ val text: Text,
+ /** Where the menu should be anchored, roughly in screen space. */
+ val position: Position,
+ /** Callback to invoke when the menu gets clicked by the user. */
+ val onClicked: () -> Unit,
+ /** Callback to invoke when the menu gets dismissed by the user. */
+ val onDismissed: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
new file mode 100644
index 0000000..bc9dc4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class LockscreenToDreamingTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_DREAMING_DURATION,
+ transitionFlow = interactor.lockscreenToDreamingTransition,
+ )
+
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return transitionAnimation.createFlow(
+ duration = 500.milliseconds,
+ onStep = { it * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
+ )
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
+
+ companion object {
+ @JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
new file mode 100644
index 0000000..a60665a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class LockscreenToOccludedTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_OCCLUDED_DURATION,
+ transitionFlow = interactor.lockscreenToOccludedTransition,
+ )
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ duration = 250.milliseconds,
+ onStep = { 1f - it },
+ )
+
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return transitionAnimation.createFlow(
+ duration = TO_OCCLUDED_DURATION,
+ onStep = { value -> value * translatePx },
+ // Reset on cancel or finish
+ onFinish = { 0f },
+ onCancel = { 0f },
+ interpolator = EMPHASIZED_ACCELERATE,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
index e804562..5770f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -20,11 +20,10 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
/**
* Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -36,28 +35,26 @@
constructor(
private val interactor: KeyguardTransitionInteractor,
) {
+ private val transitionAnimation =
+ KeyguardTransitionAnimationFlow(
+ transitionDuration = TO_LOCKSCREEN_DURATION,
+ transitionFlow = interactor.occludedToLockscreenTransition,
+ )
+
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
- -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
- }
- }
-
- /** Lockscreen views alpha */
- val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
-
- private fun flowForAnimation(params: AnimationParams): Flow<Float> {
- return interactor.transitionStepAnimation(
- interactor.occludedToLockscreenTransition,
- params,
- totalDuration = TO_LOCKSCREEN_DURATION
+ return transitionAnimation.createFlow(
+ duration = TO_LOCKSCREEN_DURATION,
+ onStep = { value -> -translatePx + value * translatePx },
+ interpolator = EMPHASIZED_DECELERATE,
)
}
- companion object {
- @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
- val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
- val LOCKSCREEN_ALPHA =
- AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
- }
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> =
+ transitionAnimation.createFlow(
+ startTime = 233.milliseconds,
+ duration = 250.milliseconds,
+ onStep = { it },
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index e364918..d69ac7f 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -145,7 +145,7 @@
* └───────────────┴───────────────────┴──────────────┴─────────────────┘
* ```
*/
-private class ViewLifecycleOwner(
+class ViewLifecycleOwner(
private val view: View,
) : LifecycleOwner {
diff --git a/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
new file mode 100644
index 0000000..5acaa46
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/ScreenDecorationsLogger.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.log
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.RectF
+import androidx.core.graphics.toRectF
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.ScreenDecorationsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import javax.inject.Inject
+
+private const val TAG = "ScreenDecorationsLog"
+
+/**
+ * Helper class for logging for [com.android.systemui.ScreenDecorations]
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ * adb shell settings put global systemui/buffer/ScreenDecorationsLog <logLevel>
+ *
+ * ```
+ */
+@SysUISingleton
+class ScreenDecorationsLogger
+@Inject
+constructor(
+ @ScreenDecorationsLog private val logBuffer: LogBuffer,
+) {
+ fun cameraProtectionBoundsForScanningOverlay(bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ { str1 = bounds.toShortString() },
+ { "Face scanning overlay present camera protection bounds: $str1" }
+ )
+ }
+
+ fun hwcLayerCameraProtectionBounds(bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ { str1 = bounds.toShortString() },
+ { "Hwc layer present camera protection bounds: $str1" }
+ )
+ }
+
+ fun dcvCameraBounds(id: Int, bounds: Rect) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = bounds.toShortString()
+ int1 = id
+ },
+ { "DisplayCutoutView id=$int1 present, camera protection bounds: $str1" }
+ )
+ }
+
+ fun cutoutViewNotInitialized() {
+ logBuffer.log(TAG, ERROR, "CutoutView not initialized showCameraProtection")
+ }
+
+ fun boundingRect(boundingRectangle: RectF, context: String) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = context
+ str2 = boundingRectangle.toShortString()
+ },
+ { "Bounding rect $str1 : $str2" }
+ )
+ }
+
+ fun boundingRect(boundingRectangle: Rect, context: String) {
+ boundingRect(boundingRectangle.toRectF(), context)
+ }
+
+ fun onMeasureDimensions(
+ widthMeasureSpec: Int,
+ heightMeasureSpec: Int,
+ measuredWidth: Int,
+ measuredHeight: Int
+ ) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ long1 = widthMeasureSpec.toLong()
+ long2 = heightMeasureSpec.toLong()
+ int1 = measuredWidth
+ int2 = measuredHeight
+ },
+ {
+ "Face scanning animation: widthMeasureSpec: $long1 measuredWidth: $int1, " +
+ "heightMeasureSpec: $long2 measuredHeight: $int2"
+ }
+ )
+ }
+
+ fun faceSensorLocation(faceSensorLocation: Point?) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = faceSensorLocation?.y?.times(2) ?: 0
+ str1 = "$faceSensorLocation"
+ },
+ { "Reinflating view: Face sensor location: $str1, faceScanningHeight: $int1" }
+ )
+ }
+
+ fun onSensorLocationChanged() {
+ logBuffer.log(TAG, DEBUG, "AuthControllerCallback in ScreenDecorations triggered")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
index 0645236..9f563fe4 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -23,3 +23,15 @@
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class KeyguardClockLog
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for small keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardSmallClockLog
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for large keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardLargeClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index bc29858..4177480 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -191,37 +191,12 @@
false /* systrace */);
}
- /**
- * Provides a logging buffer for logs related to swiping away the status bar while in immersive
- * mode. See {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
- */
+ /** Provides a logging buffer for logs related to swipe up gestures. */
@Provides
@SysUISingleton
- @SwipeStatusBarAwayLog
- public static LogBuffer provideSwipeAwayGestureLogBuffer(LogBufferFactory factory) {
- return factory.create("SwipeStatusBarAwayLog", 30);
- }
-
- /**
- * Provides a logging buffer for logs related to the media tap-to-transfer chip on the sender
- * device. See {@link com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger}.
- */
- @Provides
- @SysUISingleton
- @MediaTttSenderLogBuffer
- public static LogBuffer provideMediaTttSenderLogBuffer(LogBufferFactory factory) {
- return factory.create("MediaTttSender", 20);
- }
-
- /**
- * Provides a logging buffer for logs related to the media tap-to-transfer chip on the receiver
- * device. See {@link com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger}.
- */
- @Provides
- @SysUISingleton
- @MediaTttReceiverLogBuffer
- public static LogBuffer provideMediaTttReceiverLogBuffer(LogBufferFactory factory) {
- return factory.create("MediaTttReceiver", 20);
+ @SwipeUpLog
+ public static LogBuffer provideSwipeUpLogBuffer(LogBufferFactory factory) {
+ return factory.create("SwipeUpLog", 30);
}
/**
@@ -335,13 +310,33 @@
}
/**
- * Provides a {@link LogBuffer} for keyguard clock logs.
+ * Provides a {@link LogBuffer} for general keyguard clock logs.
*/
@Provides
@SysUISingleton
@KeyguardClockLog
public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
- return factory.create("KeyguardClockLog", 500);
+ return factory.create("KeyguardClockLog", 100);
+ }
+
+ /**
+ * Provides a {@link LogBuffer} for keyguard small clock logs.
+ */
+ @Provides
+ @SysUISingleton
+ @KeyguardSmallClockLog
+ public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) {
+ return factory.create("KeyguardSmallClockLog", 100);
+ }
+
+ /**
+ * Provides a {@link LogBuffer} for keyguard large clock logs.
+ */
+ @Provides
+ @SysUISingleton
+ @KeyguardLargeClockLog
+ public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) {
+ return factory.create("KeyguardLargeClockLog", 100);
}
/**
@@ -355,6 +350,16 @@
}
/**
+ * Provides a {@link LogBuffer} for use by {@link com.android.systemui.ScreenDecorations}.
+ */
+ @Provides
+ @SysUISingleton
+ @ScreenDecorationsLog
+ public static LogBuffer provideScreenDecorationsLog(LogBufferFactory factory) {
+ return factory.create("ScreenDecorationsLog", 200);
+ }
+
+ /**
* Provides a {@link LogBuffer} for bluetooth-related logs.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
index 67733e9..de2a8b6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ScreenDecorationsLog.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -11,15 +11,15 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.keyguard.shared.model
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.log.dagger
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
-)
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for ScreenDecorations added by SysUI. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ScreenDecorationsLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeUpLog.java
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
rename to packages/SystemUI/src/com/android/systemui/log/dagger/SwipeUpLog.java
index 4c276e2..d58b538 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeUpLog.java
@@ -27,10 +27,10 @@
/**
* A {@link LogBuffer} for
- * {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
+ * {@link com.android.systemui.statusbar.gesture.SwipeUpGestureLogger}.
*/
@Qualifier
@Documented
@Retention(RUNTIME)
-public @interface SwipeStatusBarAwayLog {
+public @interface SwipeUpLog {
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
index 7a90a74..7ccc43c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
@@ -29,6 +29,18 @@
private val dumpManager: DumpManager,
private val systemClock: SystemClock,
) {
+ private val existingBuffers = mutableMapOf<String, TableLogBuffer>()
+
+ /**
+ * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where
+ * it is guaranteed only to be created one time. See [getOrCreate] for a cache-aware method of
+ * obtaining a buffer.
+ *
+ * @param name a unique table name
+ * @param maxSize the buffer max size. See [adjustMaxSize]
+ *
+ * @return a new [TableLogBuffer] registered with [DumpManager]
+ */
fun create(
name: String,
maxSize: Int,
@@ -37,4 +49,23 @@
dumpManager.registerNormalDumpable(name, tableBuffer)
return tableBuffer
}
+
+ /**
+ * Log buffers are retained indefinitely by [DumpManager], so that they can be represented in
+ * bugreports. Because of this, many of them are created statically in the Dagger graph.
+ *
+ * In the case where you have to create a logbuffer with a name only known at runtime, this
+ * method can be used to lazily create a table log buffer which is then cached for reuse.
+ *
+ * @return a [TableLogBuffer] suitable for reuse
+ */
+ fun getOrCreate(
+ name: String,
+ maxSize: Int,
+ ): TableLogBuffer =
+ existingBuffers.getOrElse(name) {
+ val buffer = create(name, maxSize)
+ existingBuffers[name] = buffer
+ buffer
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index ceb4845..a692ad7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -18,6 +18,7 @@
import android.app.ActivityOptions
import android.content.Intent
import android.content.res.Configuration
+import android.content.res.Resources
import android.media.projection.IMediaProjection
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
import android.os.Binder
@@ -27,6 +28,7 @@
import android.os.UserHandle
import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
import com.android.internal.app.ChooserActivity
import com.android.internal.app.ResolverListController
import com.android.internal.app.chooser.NotSelectableTargetInfo
@@ -59,16 +61,12 @@
private lateinit var configurationController: ConfigurationController
private lateinit var controller: MediaProjectionAppSelectorController
private lateinit var recentsViewController: MediaProjectionRecentsViewController
+ private lateinit var component: MediaProjectionAppSelectorComponent
override fun getLayoutResource() = R.layout.media_projection_app_selector
public override fun onCreate(bundle: Bundle?) {
- val component =
- componentFactory.create(
- activity = this,
- view = this,
- resultHandler = this
- )
+ component = componentFactory.create(activity = this, view = this, resultHandler = this)
// Create a separate configuration controller for this activity as the configuration
// might be different from the global one
@@ -76,11 +74,12 @@
controller = component.controller
recentsViewController = component.recentsViewController
- val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
- intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
+ intent.configureChooserIntent(
+ resources,
+ component.hostUserHandle,
+ component.personalProfileUserHandle
+ )
- val title = getString(R.string.media_projection_permission_app_selector_title)
- intent.putExtra(Intent.EXTRA_TITLE, title)
super.onCreate(bundle)
controller.init()
}
@@ -183,6 +182,13 @@
override fun shouldShowContentPreview() = true
+ override fun shouldShowContentPreviewWhenEmpty(): Boolean = true
+
+ override fun createMyUserIdProvider(): MyUserIdProvider =
+ object : MyUserIdProvider() {
+ override fun getMyUserId(): Int = component.hostUserHandle.identifier
+ }
+
override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
recentsViewController.createView(parent)
@@ -193,6 +199,34 @@
* instance through activity result.
*/
const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
+
+ /** UID of the app that originally launched the media projection flow (host app user) */
+ const val EXTRA_HOST_APP_USER_HANDLE = "launched_from_user_handle"
const val KEY_CAPTURE_TARGET = "capture_region"
+
+ /** Set up intent for the [ChooserActivity] */
+ private fun Intent.configureChooserIntent(
+ resources: Resources,
+ hostUserHandle: UserHandle,
+ personalProfileUserHandle: UserHandle
+ ) {
+ // Specify the query intent to show icons for all apps on the chooser screen
+ val queryIntent =
+ Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
+ putExtra(Intent.EXTRA_INTENT, queryIntent)
+
+ // Update the title of the chooser
+ val title = resources.getString(R.string.media_projection_permission_app_selector_title)
+ putExtra(Intent.EXTRA_TITLE, title)
+
+ // Select host app's profile tab by default
+ val selectedProfile =
+ if (hostUserHandle == personalProfileUserHandle) {
+ PROFILE_PERSONAL
+ } else {
+ PROFILE_WORK
+ }
+ putExtra(EXTRA_SELECTED_PROFILE, selectedProfile)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index bfa67a8..d830fc4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -22,6 +22,7 @@
import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP;
import android.app.Activity;
+import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
@@ -35,6 +36,7 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.UserHandle;
import android.text.BidiFormatter;
import android.text.SpannableString;
import android.text.TextPaint;
@@ -208,8 +210,14 @@
final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class);
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
projection.asBinder());
+ intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+ UserHandle.getUserHandleForUid(getLaunchedFromUid()));
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
- startActivity(intent);
+
+ // Start activity from the current foreground user to avoid creating a separate
+ // SystemUI process without access to recent tasks because it won't have
+ // WM Shell running inside.
+ startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser()));
}
} catch (RemoteException e) {
Log.e(TAG, "Error granting projection permission", e);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index f006442..b7a2522 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -88,7 +88,13 @@
val instanceId: InstanceId,
/** The UID of the app, used for logging */
- val appUid: Int
+ val appUid: Int,
+
+ /** Whether explicit indicator exists */
+ val isExplicit: Boolean = false,
+
+ /** Track progress (0 - 1) to display for players where [resumption] is true */
+ val resumeProgress: Double? = null,
) {
companion object {
/** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
index a8f39fa9a..1c8bfd1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
@@ -24,6 +24,7 @@
import android.widget.SeekBar
import android.widget.TextView
import androidx.constraintlayout.widget.Barrier
+import com.android.internal.widget.CachingIconView
import com.android.systemui.R
import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.surfaceeffects.ripple.MultiRippleView
@@ -44,6 +45,7 @@
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
+ val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator)
// Output switcher
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
@@ -123,6 +125,7 @@
R.id.app_name,
R.id.header_title,
R.id.header_artist,
+ R.id.media_explicit_indicator,
R.id.media_seamless,
R.id.media_progress_bar,
R.id.actionPlayPause,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index bba5f35..a057c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -238,6 +238,24 @@
}
/**
+ * Set the progress to a fixed percentage value that cannot be changed by the user.
+ *
+ * @param percent value between 0 and 1
+ */
+ fun updateStaticProgress(percent: Double) {
+ val position = (percent * 100).toInt()
+ _data =
+ Progress(
+ enabled = true,
+ seekAvailable = false,
+ playing = false,
+ scrubbing = false,
+ elapsedTime = position,
+ duration = 100,
+ )
+ }
+
+ /**
* Puts the seek bar into a resumption state.
*
* This should be called when the media session behind the controller has been destroyed.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
index 1a10b18..8c1ec16 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -21,6 +21,7 @@
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
+import com.android.internal.widget.CachingIconView
import com.android.systemui.R
import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.ui.IlluminationDrawable
@@ -29,18 +30,15 @@
private const val TAG = "RecommendationViewHolder"
/** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View) {
+class RecommendationViewHolder private constructor(itemView: View, updatedView: Boolean) {
val recommendations = itemView as TransitionLayout
// Recommendation screen
- val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
- val mediaCoverItems =
- listOf<ImageView>(
- itemView.requireViewById(R.id.media_cover1),
- itemView.requireViewById(R.id.media_cover2),
- itemView.requireViewById(R.id.media_cover3)
- )
+ lateinit var cardIcon: ImageView
+ lateinit var mediaAppIcons: List<CachingIconView>
+ lateinit var cardTitle: TextView
+
val mediaCoverContainers =
listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover1_container),
@@ -48,21 +46,45 @@
itemView.requireViewById(R.id.media_cover3_container)
)
val mediaTitles: List<TextView> =
- listOf(
- itemView.requireViewById(R.id.media_title1),
- itemView.requireViewById(R.id.media_title2),
- itemView.requireViewById(R.id.media_title3)
- )
+ if (updatedView) {
+ mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
+ } else {
+ listOf(
+ itemView.requireViewById(R.id.media_title1),
+ itemView.requireViewById(R.id.media_title2),
+ itemView.requireViewById(R.id.media_title3)
+ )
+ }
val mediaSubtitles: List<TextView> =
- listOf(
- itemView.requireViewById(R.id.media_subtitle1),
- itemView.requireViewById(R.id.media_subtitle2),
- itemView.requireViewById(R.id.media_subtitle3)
- )
+ if (updatedView) {
+ mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
+ } else {
+ listOf(
+ itemView.requireViewById(R.id.media_subtitle1),
+ itemView.requireViewById(R.id.media_subtitle2),
+ itemView.requireViewById(R.id.media_subtitle3)
+ )
+ }
+ val mediaCoverItems: List<ImageView> =
+ if (updatedView) {
+ mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
+ } else {
+ listOf(
+ itemView.requireViewById(R.id.media_cover1),
+ itemView.requireViewById(R.id.media_cover2),
+ itemView.requireViewById(R.id.media_cover3)
+ )
+ }
val gutsViewHolder = GutsViewHolder(itemView)
init {
+ if (updatedView) {
+ mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
+ cardTitle = itemView.requireViewById(R.id.media_rec_title)
+ } else {
+ cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
+ }
(recommendations.background as IlluminationDrawable).let { background ->
mediaCoverContainers.forEach { background.registerLightSource(it) }
background.registerLightSource(gutsViewHolder.cancel)
@@ -83,36 +105,52 @@
* @param parent Parent of inflated view.
*/
@JvmStatic
- fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
+ fun create(
+ inflater: LayoutInflater,
+ parent: ViewGroup,
+ updatedView: Boolean,
+ ): RecommendationViewHolder {
val itemView =
- inflater.inflate(
- R.layout.media_smartspace_recommendations,
- parent,
- false /* attachToRoot */
- )
+ if (updatedView) {
+ inflater.inflate(
+ R.layout.media_recommendations,
+ parent,
+ false /* attachToRoot */
+ )
+ } else {
+ inflater.inflate(
+ R.layout.media_smartspace_recommendations,
+ parent,
+ false /* attachToRoot */
+ )
+ }
// Because this media view (a TransitionLayout) is used to measure and layout the views
// in various states before being attached to its parent, we can't depend on the default
// LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
- return RecommendationViewHolder(itemView)
+ return RecommendationViewHolder(itemView, updatedView)
}
// Res Ids for the control components on the recommendation view.
val controlsIds =
setOf(
R.id.recommendation_card_icon,
+ R.id.media_rec_title,
R.id.media_cover1,
R.id.media_cover2,
R.id.media_cover3,
+ R.id.media_cover,
R.id.media_cover1_container,
R.id.media_cover2_container,
R.id.media_cover3_container,
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
+ R.id.media_title,
R.id.media_subtitle1,
R.id.media_subtitle2,
- R.id.media_subtitle3
+ R.id.media_subtitle3,
+ R.id.media_subtitle,
)
val mediaTitlesAndSubtitlesIds =
@@ -120,9 +158,11 @@
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
+ R.id.media_title,
R.id.media_subtitle1,
R.id.media_subtitle2,
- R.id.media_subtitle3
+ R.id.media_subtitle3,
+ R.id.media_subtitle,
)
val mediaContainersIds =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 2dd339d..aba3e98 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -45,6 +45,7 @@
import android.os.UserHandle
import android.provider.Settings
import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
import android.text.TextUtils
import android.util.Log
import androidx.media.utils.MediaConstants
@@ -66,6 +67,7 @@
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener
import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter
@@ -302,6 +304,7 @@
mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
updateState(key, state)
}
+ mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
mediaResumeListener.setManager(this)
mediaDataFilter.mediaDataManager = this
@@ -660,6 +663,15 @@
val currentEntry = mediaEntries.get(packageName)
val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+ val isExplicit =
+ desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
+ mediaFlags.isExplicitIndicatorEnabled()
+
+ val progress =
+ if (mediaFlags.isResumeProgressEnabled()) {
+ MediaDataUtils.getDescriptionProgress(desc.extras)
+ } else null
val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime()
@@ -689,7 +701,9 @@
hasCheckedForResume = true,
lastActive = lastActive,
instanceId = instanceId,
- appUid = appUid
+ appUid = appUid,
+ isExplicit = isExplicit,
+ resumeProgress = progress,
)
)
}
@@ -750,6 +764,15 @@
song = HybridGroupManager.resolveTitle(notif)
}
+ // Explicit Indicator
+ var isExplicit = false
+ if (mediaFlags.isExplicitIndicatorEnabled()) {
+ val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+ isExplicit =
+ mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+ }
+
// Artist name
var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
if (artist == null) {
@@ -848,10 +871,11 @@
notificationKey = key,
hasCheckedForResume = hasCheckedForResume,
isPlaying = isPlaying,
- isClearable = sbn.isClearable(),
+ isClearable = !sbn.isOngoing,
lastActive = lastActive,
instanceId = instanceId,
- appUid = appUid
+ appUid = appUid,
+ isExplicit = isExplicit,
)
)
}
@@ -1273,45 +1297,106 @@
fun onNotificationRemoved(key: String) {
Assert.isMainThread()
- val removed = mediaEntries.remove(key)
- if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
- Log.d(TAG, "Not removing $key because resumable")
- // Move to resume key (aka package name) if that key doesn't already exist.
- val resumeAction = getResumeMediaAction(removed.resumeAction!!)
- val updated =
- removed.copy(
- token = null,
- actions = listOf(resumeAction),
- semanticActions = MediaButton(playOrPause = resumeAction),
- actionsToShowInCompact = listOf(0),
- active = false,
- resumption = true,
- isPlaying = false,
- isClearable = true
- )
- val pkg = removed.packageName
- val migrate = mediaEntries.put(pkg, updated) == null
- // Notify listeners of "new" controls when migrating or removed and update when not
- if (migrate) {
- notifyMediaDataLoaded(pkg, key, updated)
- } else {
- // Since packageName is used for the key of the resumption controls, it is
- // possible that another notification has already been reused for the resumption
- // controls of this package. In this case, rather than renaming this player as
- // packageName, just remove it and then send a update to the existing resumption
- // controls.
- notifyMediaDataRemoved(key)
- notifyMediaDataLoaded(pkg, pkg, updated)
- }
- logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
- return
- }
- if (removed != null) {
+ val removed = mediaEntries.remove(key) ?: return
+
+ if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) {
+ convertToResumePlayer(removed)
+ } else if (mediaFlags.isRetainingPlayersEnabled()) {
+ handlePossibleRemoval(removed, notificationRemoved = true)
+ } else {
notifyMediaDataRemoved(key)
logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
}
}
+ private fun onSessionDestroyed(key: String) {
+ if (!mediaFlags.isRetainingPlayersEnabled()) return
+
+ if (DEBUG) Log.d(TAG, "session destroyed for $key")
+ val entry = mediaEntries.remove(key) ?: return
+ // Clear token since the session is no longer valid
+ val updated = entry.copy(token = null)
+ handlePossibleRemoval(updated)
+ }
+
+ /**
+ * Convert to resume state if the player is no longer valid and active, then notify listeners
+ * that the data was updated. Does not convert to resume state if the player is still valid, or
+ * if it was removed before becoming inactive. (Assumes that [removed] was removed from
+ * [mediaEntries] before this function was called)
+ */
+ private fun handlePossibleRemoval(removed: MediaData, notificationRemoved: Boolean = false) {
+ val key = removed.notificationKey!!
+ val hasSession = removed.token != null
+ if (hasSession && removed.semanticActions != null) {
+ // The app was using session actions, and the session is still valid: keep player
+ if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
+ mediaEntries.put(key, removed)
+ notifyMediaDataLoaded(key, key, removed)
+ } else if (!notificationRemoved && removed.semanticActions == null) {
+ // The app was using notification actions, and notif wasn't removed yet: keep player
+ if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
+ mediaEntries.put(key, removed)
+ notifyMediaDataLoaded(key, key, removed)
+ } else if (removed.active) {
+ // This player was still active - it didn't last long enough to time out: remove
+ if (DEBUG) Log.d(TAG, "Removing still-active player $key")
+ notifyMediaDataRemoved(key)
+ logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+ } else {
+ // Convert to resume
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Notification ($notificationRemoved) and/or session " +
+ "($hasSession) gone for inactive player $key"
+ )
+ }
+ convertToResumePlayer(removed)
+ }
+ }
+
+ /** Set the given [MediaData] as a resume state player and notify listeners */
+ private fun convertToResumePlayer(data: MediaData) {
+ val key = data.notificationKey!!
+ if (DEBUG) Log.d(TAG, "Converting $key to resume")
+ // Move to resume key (aka package name) if that key doesn't already exist.
+ val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
+ val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
+ val launcherIntent =
+ context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
+ PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+ }
+ val updated =
+ data.copy(
+ token = null,
+ actions = actions,
+ semanticActions = MediaButton(playOrPause = resumeAction),
+ actionsToShowInCompact = listOf(0),
+ active = false,
+ resumption = true,
+ isPlaying = false,
+ isClearable = true,
+ clickIntent = launcherIntent,
+ )
+ val pkg = data.packageName
+ val migrate = mediaEntries.put(pkg, updated) == null
+ // Notify listeners of "new" controls when migrating or removed and update when not
+ Log.d(TAG, "migrating? $migrate from $key -> $pkg")
+ if (migrate) {
+ notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
+ } else {
+ // Since packageName is used for the key of the resumption controls, it is
+ // possible that another notification has already been reused for the resumption
+ // controls of this package. In this case, rather than renaming this player as
+ // packageName, just remove it and then send a update to the existing resumption
+ // controls.
+ notifyMediaDataRemoved(key)
+ notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
+ }
+ logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+ }
+
fun setMediaResumptionEnabled(isEnabled: Boolean) {
if (useMediaResumption == isEnabled) {
return
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
index 7f5c82f..a898b00 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
@@ -71,6 +71,12 @@
*/
lateinit var stateCallback: (String, PlaybackState) -> Unit
+ /**
+ * Callback representing that the [MediaSession] for an active control has been destroyed
+ * @param key Media control unique identifier
+ */
+ lateinit var sessionCallback: (String) -> Unit
+
init {
statusBarStateController.addCallback(
object : StatusBarStateController.StateListener {
@@ -211,6 +217,7 @@
} else {
// For active controls, if the session is destroyed, clean up everything since we
// will need to recreate it if this key is updated later
+ sessionCallback.invoke(key)
destroy()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
index 93be6a7..4827a16 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
@@ -162,8 +162,8 @@
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
UI_MODE_NIGHT_YES
)
- colorScheme.accent1[2]
- else colorScheme.accent1[3]
+ colorScheme.accent1.s100
+ else colorScheme.accent1.s200
},
{ seamlessColor: Int ->
val accentColorList = ColorStateList.valueOf(seamlessColor)
@@ -230,7 +230,14 @@
fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
var anyChanged = false
- colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged }
+ colorTransitions.forEach {
+ val isChanged = it.updateColorScheme(colorScheme)
+
+ // Ignore changes to colorSeamless, since that is expected when toggling dark mode
+ if (it == colorSeamless) return@forEach
+
+ anyChanged = isChanged || anyChanged
+ }
colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
return anyChanged
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index 899148b..8f1c904 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -130,7 +130,12 @@
private var splitShadeContainer: ViewGroup? = null
/** Track the media player setting status on lock screen. */
- private var allowMediaPlayerOnLockScreen: Boolean = true
+ private var allowMediaPlayerOnLockScreen: Boolean =
+ secureSettings.getBoolForUser(
+ Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+ true,
+ UserHandle.USER_CURRENT
+ )
private val lockScreenMediaPlayerUri =
secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index 1fdbc99..b2ad155 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -43,6 +43,7 @@
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.SmallHash
import com.android.systemui.plugins.ActivityStarter
@@ -88,13 +89,14 @@
falsingManager: FalsingManager,
dumpManager: DumpManager,
private val logger: MediaUiEventLogger,
- private val debugLogger: MediaCarouselControllerLogger
+ private val debugLogger: MediaCarouselControllerLogger,
+ private val mediaFlags: MediaFlags,
) : Dumpable {
/** The current width of the carousel */
private var currentCarouselWidth: Int = 0
/** The current height of the carousel */
- private var currentCarouselHeight: Int = 0
+ @VisibleForTesting var currentCarouselHeight: Int = 0
/** Are we currently showing only active players */
private var currentlyShowingOnlyActive: Boolean = false
@@ -128,14 +130,14 @@
/** The measured height of the carousel */
private var carouselMeasureHeight: Int = 0
private var desiredHostState: MediaHostState? = null
- private val mediaCarousel: MediaScrollView
+ @VisibleForTesting var mediaCarousel: MediaScrollView
val mediaCarouselScrollHandler: MediaCarouselScrollHandler
val mediaFrame: ViewGroup
@VisibleForTesting
lateinit var settingsButton: View
private set
private val mediaContent: ViewGroup
- @VisibleForTesting val pageIndicator: PageIndicator
+ @VisibleForTesting var pageIndicator: PageIndicator
private val visualStabilityCallback: OnReorderingAllowedListener
private var needsReordering: Boolean = false
private var keysNeedRemoval = mutableSetOf<String>()
@@ -160,30 +162,26 @@
}
companion object {
- const val ANIMATION_BASE_DURATION = 2200f
- const val DURATION = 167f
- const val DETAILS_DELAY = 1067f
- const val CONTROLS_DELAY = 1400f
- const val PAGINATION_DELAY = 1900f
- const val MEDIATITLES_DELAY = 1000f
- const val MEDIACONTAINERS_DELAY = 967f
val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F)
- val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F)
- fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
- val transformStartFraction = delay / ANIMATION_BASE_DURATION
- val transformDurationFraction = duration / ANIMATION_BASE_DURATION
- val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
- return MathUtils.constrain(
- (squishinessToTime - transformStartFraction) / transformDurationFraction,
- 0F,
- 1F
- )
+ fun calculateAlpha(
+ squishinessFraction: Float,
+ startPosition: Float,
+ endPosition: Float
+ ): Float {
+ val transformFraction =
+ MathUtils.constrain(
+ (squishinessFraction - startPosition) / (endPosition - startPosition),
+ 0F,
+ 1F
+ )
+ return TRANSFORM_BEZIER.getInterpolation(transformFraction)
}
}
private val configListener =
object : ConfigurationController.ConfigurationListener {
+ var lastOrientation = -1
override fun onDensityOrFontScaleChanged() {
// System font changes should only happen when UMO is offscreen or a flicker may
@@ -200,7 +198,13 @@
override fun onConfigChanged(newConfig: Configuration?) {
if (newConfig == null) return
isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
- updatePlayers(recreateMedia = true)
+ val newOrientation = newConfig.orientation
+ if (lastOrientation != newOrientation) {
+ // The players actually depend on the orientation possibly, so we have to
+ // recreate them (at least on large screen devices)
+ lastOrientation = newOrientation
+ updatePlayers(recreateMedia = true)
+ }
}
override fun onUiModeChanged() {
@@ -645,7 +649,11 @@
val newRecs = mediaControlPanelFactory.get()
newRecs.attachRecommendation(
- RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
+ RecommendationViewHolder.create(
+ LayoutInflater.from(context),
+ mediaContent,
+ mediaFlags.isRecommendationCardUpdateEnabled()
+ )
)
newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
val lp =
@@ -717,6 +725,9 @@
private fun updatePlayers(recreateMedia: Boolean) {
pageIndicator.tintList =
ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+ val previousVisibleKey =
+ MediaPlayerData.visiblePlayerKeys()
+ .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
if (isSsMediaRec) {
@@ -741,6 +752,9 @@
isSsReactivated = isSsReactivated
)
}
+ if (recreateMedia) {
+ reorderAllPlayers(previousVisibleKey)
+ }
}
}
@@ -800,7 +814,12 @@
val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
val endAlpha =
(if (endIsVisible) 1.0f else 0.0f) *
- calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
+ calculateAlpha(
+ squishFraction,
+ (pageIndicator.translationY + pageIndicator.height) /
+ mediaCarousel.measuredHeight,
+ 1F
+ )
var alpha = 1.0f
if (!endIsVisible || !startIsVisible) {
var progress = currentTransitionProgress
@@ -826,7 +845,8 @@
pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
pageIndicator.translationY =
- (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat()
+ (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin)
+ .toFloat()
}
/** Update the dimension of this carousel. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
index 82abf9b..2a8362b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
@@ -19,28 +19,28 @@
import com.android.systemui.monet.ColorScheme
/** Returns the surface color for media controls based on the scheme. */
-internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2[9] // A2-800
+internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2.s800 // A2-800
/** Returns the primary accent color for media controls based on the scheme. */
-internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1[2] // A1-100
+internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1.s100 // A1-100
/** Returns the secondary accent color for media controls based on the scheme. */
-internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1[3] // A1-200
+internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1.s200 // A1-200
/** Returns the primary text color for media controls based on the scheme. */
-internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1[1] // N1-50
+internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1.s50 // N1-50
/** Returns the inverse of the primary text color for media controls based on the scheme. */
-internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1[10] // N1-900
+internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1.s900 // N1-900
/** Returns the secondary text color for media controls based on the scheme. */
-internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2[3] // N2-200
+internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s200 // N2-200
/** Returns the tertiary text color for media controls based on the scheme. */
-internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2[5] // N2-400
+internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2.s400 // N2-400
/** Returns the color for the start of the background gradient based on the scheme. */
-internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2[8] // A2-700
+internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2.s700 // A2-700
/** Returns the color for the end of the background gradient based on the scheme. */
-internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1[8] // A1-700
+internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1.s700 // A1-700
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 15c3443..d26f239 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -50,7 +50,6 @@
import android.os.Trace;
import android.text.TextUtils;
import android.util.Log;
-import android.util.Pair;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
@@ -62,12 +61,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import androidx.appcompat.content.res.AppCompatResources;
import androidx.constraintlayout.widget.ConstraintSet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.InstanceId;
+import com.android.internal.widget.CachingIconView;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.R;
@@ -111,6 +112,7 @@
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController;
import com.android.systemui.util.ColorUtilKt;
import com.android.systemui.util.animation.TransitionLayout;
+import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.time.SystemClock;
import java.net.URISyntaxException;
@@ -121,6 +123,7 @@
import javax.inject.Inject;
import dagger.Lazy;
+import kotlin.Triple;
import kotlin.Unit;
/**
@@ -166,10 +169,13 @@
R.id.action1
);
+ // Time in millis for playing turbulence noise that is played after a touch ripple.
+ @VisibleForTesting static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
+
private final SeekBarViewModel mSeekBarViewModel;
private SeekBarObserver mSeekBarObserver;
protected final Executor mBackgroundExecutor;
- private final Executor mMainExecutor;
+ private final DelayableExecutor mMainExecutor;
private final ActivityStarter mActivityStarter;
private final BroadcastSender mBroadcastSender;
@@ -222,10 +228,10 @@
private String mSwitchBroadcastApp;
private MultiRippleController mMultiRippleController;
private TurbulenceNoiseController mTurbulenceNoiseController;
- private FeatureFlags mFeatureFlags;
- private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null;
+ private final FeatureFlags mFeatureFlags;
+ private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig;
@VisibleForTesting
- MultiRippleController.Companion.RipplesFinishedListener mRipplesFinishedListener = null;
+ MultiRippleController.Companion.RipplesFinishedListener mRipplesFinishedListener;
/**
* Initialize a new control panel
@@ -239,7 +245,7 @@
public MediaControlPanel(
Context context,
@Background Executor backgroundExecutor,
- @Main Executor mainExecutor,
+ @Main DelayableExecutor mainExecutor,
ActivityStarter activityStarter,
BroadcastSender broadcastSender,
MediaViewController mediaViewController,
@@ -398,10 +404,11 @@
TextView titleText = mMediaViewHolder.getTitleText();
TextView artistText = mMediaViewHolder.getArtistText();
+ CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator();
AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
- Interpolators.EMPHASIZED_DECELERATE, titleText, artistText);
+ Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator);
AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
- Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
+ Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator);
MultiRippleView multiRippleView = vh.getMultiRippleView();
mMultiRippleController = new MultiRippleController(multiRippleView);
@@ -409,10 +416,12 @@
if (mFeatureFlags.isEnabled(Flags.UMO_TURBULENCE_NOISE)) {
mRipplesFinishedListener = () -> {
if (mTurbulenceNoiseAnimationConfig == null) {
- mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation();
+ mTurbulenceNoiseAnimationConfig = createTurbulenceNoiseAnimation();
}
// Color will be correctly updated in ColorSchemeTransition.
mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig);
+ mMainExecutor.executeDelayed(
+ mTurbulenceNoiseController::finish, TURBULENCE_NOISE_PLAY_DURATION);
};
mMultiRippleController.addRipplesFinishedListener(mRipplesFinishedListener);
}
@@ -513,8 +522,13 @@
}
// Seek Bar
- final MediaController controller = getController();
- mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+ if (data.getResumption() && data.getResumeProgress() != null) {
+ double progress = data.getResumeProgress();
+ mSeekBarViewModel.updateStaticProgress(progress);
+ } else {
+ final MediaController controller = getController();
+ mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+ }
// Show the broadcast dialog button only when the le audio is enabled.
mShowBroadcastDialogButton =
@@ -664,11 +678,15 @@
private boolean bindSongMetadata(MediaData data) {
TextView titleText = mMediaViewHolder.getTitleText();
TextView artistText = mMediaViewHolder.getArtistText();
+ ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+ ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
return mMetadataAnimationHandler.setNext(
- Pair.create(data.getSong(), data.getArtist()),
+ new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
() -> {
titleText.setText(data.getSong());
artistText.setText(data.getArtist());
+ setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit());
+ setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit());
// refreshState is required here to resize the text views (and prevent ellipsis)
mMediaViewController.refreshState();
@@ -739,43 +757,16 @@
int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
- // WallpaperColors.fromBitmap takes a good amount of time. We do that work
- // on the background executor to avoid stalling animations on the UI Thread.
mBackgroundExecutor.execute(() -> {
// Album art
ColorScheme mutableColorScheme = null;
Drawable artwork;
boolean isArtworkBound;
Icon artworkIcon = data.getArtwork();
- WallpaperColors wallpaperColors = null;
- if (artworkIcon != null) {
- if (artworkIcon.getType() == Icon.TYPE_BITMAP
- || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
- // Avoids extra processing if this is already a valid bitmap
- wallpaperColors = WallpaperColors
- .fromBitmap(artworkIcon.getBitmap());
- } else {
- Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
- if (artworkDrawable != null) {
- wallpaperColors = WallpaperColors
- .fromDrawable(artworkIcon.loadDrawable(mContext));
- }
- }
- }
+ WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
if (wallpaperColors != null) {
mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
- Drawable albumArt = getScaledBackground(artworkIcon, width, height);
- GradientDrawable gradient = (GradientDrawable) mContext
- .getDrawable(R.drawable.qs_media_scrim);
- gradient.setColors(new int[] {
- ColorUtilKt.getColorWithAlpha(
- MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
- 0.25f),
- ColorUtilKt.getColorWithAlpha(
- MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
- 0.9f),
- });
- artwork = new LayerDrawable(new Drawable[] { albumArt, gradient });
+ artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
isArtworkBound = true;
} else {
// If there's no artwork, use colors from the app icon
@@ -854,6 +845,96 @@
});
}
+ private void bindRecommendationArtwork(
+ SmartspaceAction recommendation,
+ String packageName,
+ int itemIndex
+ ) {
+ final int traceCookie = recommendation.hashCode();
+ final String traceName =
+ "MediaControlPanel#bindRecommendationArtwork<" + packageName + ">";
+ Trace.beginAsyncSection(traceName, traceCookie);
+
+ // Capture width & height from views in foreground for artwork scaling in background
+ int width = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredWidth();
+ int height = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredHeight();
+
+ mBackgroundExecutor.execute(() -> {
+ // Album art
+ ColorScheme mutableColorScheme = null;
+ Drawable artwork;
+ Icon artworkIcon = recommendation.getIcon();
+ WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
+ if (wallpaperColors != null) {
+ mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
+ artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
+ } else {
+ artwork = new ColorDrawable(Color.TRANSPARENT);
+ }
+
+ mMainExecutor.execute(() -> {
+ // Bind the artwork drawable to media cover.
+ ImageView mediaCover =
+ mRecommendationViewHolder.getMediaCoverItems().get(itemIndex);
+ mediaCover.setImageDrawable(artwork);
+
+ // Set up the app icon.
+ ImageView appIconView = mRecommendationViewHolder.getMediaAppIcons().get(itemIndex);
+ appIconView.clearColorFilter();
+ try {
+ Drawable icon = mContext.getPackageManager()
+ .getApplicationIcon(packageName);
+ appIconView.setImageDrawable(icon);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Cannot find icon for package " + packageName, e);
+ appIconView.setImageResource(R.drawable.ic_music_note);
+ }
+ Trace.endAsyncSection(traceName, traceCookie);
+ });
+ });
+ }
+
+ // This method should be called from a background thread. WallpaperColors.fromBitmap takes a
+ // good amount of time. We do that work on the background executor to avoid stalling animations
+ // on the UI Thread.
+ private WallpaperColors getWallpaperColor(Icon artworkIcon) {
+ if (artworkIcon != null) {
+ if (artworkIcon.getType() == Icon.TYPE_BITMAP
+ || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
+ // Avoids extra processing if this is already a valid bitmap
+ return WallpaperColors
+ .fromBitmap(artworkIcon.getBitmap());
+ } else {
+ Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
+ if (artworkDrawable != null) {
+ return WallpaperColors
+ .fromDrawable(artworkIcon.loadDrawable(mContext));
+ }
+ }
+ }
+ return null;
+ }
+
+ private LayerDrawable addGradientToIcon(
+ Icon artworkIcon,
+ ColorScheme mutableColorScheme,
+ int width,
+ int height
+ ) {
+ Drawable albumArt = getScaledBackground(artworkIcon, width, height);
+ GradientDrawable gradient = (GradientDrawable) AppCompatResources
+ .getDrawable(mContext, R.drawable.qs_media_scrim);
+ gradient.setColors(new int[] {
+ ColorUtilKt.getColorWithAlpha(
+ MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
+ 0.25f),
+ ColorUtilKt.getColorWithAlpha(
+ MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
+ 0.9f),
+ });
+ return new LayerDrawable(new Drawable[] { albumArt, gradient });
+ }
+
private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
int targetWidth, int targetHeight) {
Drawable drawable = transitionDrawable.getDrawable(layer);
@@ -1056,7 +1137,7 @@
);
}
- private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() {
+ private TurbulenceNoiseAnimationConfig createTurbulenceNoiseAnimation() {
return new TurbulenceNoiseAnimationConfig(
TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT,
TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
@@ -1071,7 +1152,9 @@
/* width= */ mMediaViewHolder.getMultiRippleView().getWidth(),
/* height= */ mMediaViewHolder.getMultiRippleView().getHeight(),
TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
+ /* easeInDuration= */
TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS,
+ /* easeOutDuration= */
TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS,
this.getContext().getResources().getDisplayMetrics().density,
BlendMode.PLUS,
@@ -1209,8 +1292,10 @@
PackageManager packageManager = mContext.getPackageManager();
// Set up media source app's logo.
Drawable icon = packageManager.getApplicationIcon(applicationInfo);
- ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
- headerLogoImageView.setImageDrawable(icon);
+ if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
+ headerLogoImageView.setImageDrawable(icon);
+ }
fetchAndUpdateRecommendationColors(icon);
// Set up media rec card's tap action if applicable.
@@ -1230,7 +1315,15 @@
// Set up media item cover.
ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
- mediaCoverImageView.setImageIcon(recommendation.getIcon());
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ bindRecommendationArtwork(
+ recommendation,
+ data.getPackageName(),
+ itemIndex
+ );
+ } else {
+ mediaCoverImageView.setImageIcon(recommendation.getIcon());
+ }
// Set up the media item's click listener if applicable.
ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
@@ -1260,7 +1353,6 @@
recommendation.getTitle(), artistName, appName));
}
-
// Set up title
CharSequence title = recommendation.getTitle();
hasTitle |= !TextUtils.isEmpty(title);
@@ -1338,6 +1430,10 @@
int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
+ if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
+ mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
+ }
+
mRecommendationViewHolder.getRecommendations()
.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
mRecommendationViewHolder.getMediaTitles().forEach(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index f7a9bc7..66f12d6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -41,6 +41,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dreams.DreamOverlayStateController
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.dream.MediaDreamComplication
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeStateEvents
@@ -93,6 +94,7 @@
private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController,
private val mediaCarouselController: MediaCarouselController,
+ private val mediaManager: MediaDataManager,
private val keyguardViewController: KeyguardViewController,
private val dreamOverlayStateController: DreamOverlayStateController,
configurationController: ConfigurationController,
@@ -224,9 +226,9 @@
private var inSplitShade = false
- /** Is there any active media in the carousel? */
- private var hasActiveMedia: Boolean = false
- get() = mediaHosts.get(LOCATION_QQS)?.visible == true
+ /** Is there any active media or recommendation in the carousel? */
+ private var hasActiveMediaOrRecommendation: Boolean = false
+ get() = mediaManager.hasActiveMediaOrRecommendation()
/** Are we currently waiting on an animation to start? */
private var animationPending: Boolean = false
@@ -582,12 +584,8 @@
val viewHost = createUniqueObjectHost()
mediaObject.hostView = viewHost
mediaObject.addVisibilityChangeListener {
- // If QQS changes visibility, we need to force an update to ensure the transition
- // goes into the correct state
- val stateUpdate = mediaObject.location == LOCATION_QQS
-
// Never animate because of a visibility change, only state changes should do that
- updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
+ updateDesiredLocation(forceNoAnimation = true)
}
mediaHosts[mediaObject.location] = mediaObject
if (mediaObject.location == desiredLocation) {
@@ -908,7 +906,7 @@
fun isCurrentlyInGuidedTransformation(): Boolean {
return hasValidStartAndEndLocations() &&
getTransformationProgress() >= 0 &&
- areGuidedTransitionHostsVisible()
+ (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation)
}
private fun hasValidStartAndEndLocations(): Boolean {
@@ -965,7 +963,7 @@
private fun getQSTransformationProgress(): Float {
val currentHost = getHost(desiredLocation)
val previousHost = getHost(previousLocation)
- if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
+ if (currentHost?.location == LOCATION_QS && !inSplitShade) {
if (previousHost?.location == LOCATION_QQS) {
if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
return qsExpansion
@@ -1028,7 +1026,8 @@
private fun updateHostAttachment() =
traceSection("MediaHierarchyManager#updateHostAttachment") {
var newLocation = resolveLocationForFading()
- var canUseOverlay = !isCurrentlyFading()
+ // Don't use the overlay when fading or when we don't have active media
+ var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation
if (isCrossFadeAnimatorRunning) {
if (
getHost(newLocation)?.visible == true &&
@@ -1122,7 +1121,6 @@
dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
(qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
- !hasActiveMedia -> LOCATION_QS
onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 3224213..1e6002c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -24,18 +24,16 @@
import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.animation.TransitionLayoutController
import com.android.systemui.util.animation.TransitionViewState
import com.android.systemui.util.traceSection
+import java.lang.Float.max
+import java.lang.Float.min
import javax.inject.Inject
/**
@@ -48,7 +46,8 @@
private val context: Context,
private val configurationController: ConfigurationController,
private val mediaHostStatesManager: MediaHostStatesManager,
- private val logger: MediaViewLogger
+ private val logger: MediaViewLogger,
+ private val mediaFlags: MediaFlags,
) {
/**
@@ -80,6 +79,7 @@
setOf(
R.id.header_title,
R.id.header_artist,
+ R.id.media_explicit_indicator,
R.id.actionPlayPause,
)
@@ -304,42 +304,109 @@
val squishedViewState = viewState.copy()
val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt()
squishedViewState.height = squishedHeight
- controlIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
- }
- }
-
- detailIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
- }
- }
-
// We are not overriding the squishedViewStates height but only the children to avoid
// them remeasuring the whole view. Instead it just remains as the original size
backgroundIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.height = squishedHeight
- }
+ squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight }
}
- RecommendationViewHolder.mediaContainersIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
- }
- }
-
- RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
- squishedViewState.widgetStates.get(id)?.let { state ->
- state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
- }
- }
-
+ // media player
+ val controlsTop =
+ calculateWidgetGroupAlphaForSquishiness(
+ controlIds,
+ squishedViewState.measureHeight.toFloat(),
+ squishedViewState,
+ squishFraction
+ )
+ calculateWidgetGroupAlphaForSquishiness(
+ detailIds,
+ controlsTop,
+ squishedViewState,
+ squishFraction
+ )
+ // recommendation card
+ val titlesTop =
+ calculateWidgetGroupAlphaForSquishiness(
+ RecommendationViewHolder.mediaTitlesAndSubtitlesIds,
+ squishedViewState.measureHeight.toFloat(),
+ squishedViewState,
+ squishFraction
+ )
+ calculateWidgetGroupAlphaForSquishiness(
+ RecommendationViewHolder.mediaContainersIds,
+ titlesTop,
+ squishedViewState,
+ squishFraction
+ )
return squishedViewState
}
/**
+ * This function is to make each widget in UMO disappear before being clipped by squished UMO
+ *
+ * The general rule is that widgets in UMO has been divided into several groups, and widgets in
+ * one group have the same alpha during squishing It will change from alpha 0.0 when the visible
+ * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible
+ * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause
+ * button will change alpha together.
+ * ```
+ * And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls,
+ * including progress bar, next button, previous button
+ * ```
+ * widgetGroupIds: a group of widgets have same state during UMO is squished,
+ * ```
+ * e.g. Album title, artist title and play-pause button
+ * ```
+ * groupEndPosition: the height of UMO, when the height reaches this value,
+ * ```
+ * widgets in this group should have 1.0 as alpha
+ * e.g., the group of album title, artist title and play-pause button will become fully
+ * visible when the height of UMO reaches the top of controls group
+ * (progress bar, previous button and next button)
+ * ```
+ * squishedViewState: hold the widgetState of each widget, which will be modified
+ * squishFraction: the squishFraction of UMO
+ */
+ private fun calculateWidgetGroupAlphaForSquishiness(
+ widgetGroupIds: Set<Int>,
+ groupEndPosition: Float,
+ squishedViewState: TransitionViewState,
+ squishFraction: Float
+ ): Float {
+ val nonsquishedHeight = squishedViewState.measureHeight
+ var groupTop = squishedViewState.measureHeight.toFloat()
+ var groupBottom = 0F
+ widgetGroupIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ groupTop = min(groupTop, state.y)
+ groupBottom = max(groupBottom, state.y + state.height)
+ }
+ }
+ // startPosition means to the height of squished UMO where the widget alpha should start
+ // changing from 0.0
+ // generally, it equals to the bottom of widgets, so that we can meet the requirement that
+ // widget should not go beyond the bounds of background
+ // endPosition means to the height of squished UMO where the widget alpha should finish
+ // changing alpha to 1.0
+ var startPosition = groupBottom
+ val endPosition = groupEndPosition
+ if (startPosition == endPosition) {
+ startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat()
+ }
+ widgetGroupIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha =
+ calculateAlpha(
+ squishFraction,
+ startPosition / nonsquishedHeight,
+ endPosition / nonsquishedHeight
+ )
+ }
+ }
+ return groupTop // used for the widget group above this group
+ }
+
+ /**
* Obtain a new viewState for a given media state. This usually returns a cached state, but if
* it's not available, it will recreate one by measuring, which may be expensive.
*/
@@ -544,11 +611,13 @@
overrideSize?.let {
// To be safe we're using a maximum here. The override size should always be set
// properly though.
- if (result.measureHeight != it.measuredHeight
- || result.measureWidth != it.measuredWidth) {
+ if (
+ result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth
+ ) {
result.measureHeight = Math.max(it.measuredHeight, result.measureHeight)
result.measureWidth = Math.max(it.measuredWidth, result.measureWidth)
- // The measureHeight and the shown height should both be set to the overridden height
+ // The measureHeight and the shown height should both be set to the overridden
+ // height
result.height = result.measureHeight
result.width = result.measureWidth
// Make sure all background views are also resized such that their size is correct
@@ -579,8 +648,13 @@
expandedLayout.load(context, R.xml.media_session_expanded)
}
TYPE.RECOMMENDATION -> {
- collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
- expandedLayout.load(context, R.xml.media_recommendation_expanded)
+ if (mediaFlags.isRecommendationCardUpdateEnabled()) {
+ collapsedLayout.load(context, R.xml.media_recommendations_view_collapsed)
+ expandedLayout.load(context, R.xml.media_recommendations_view_expanded)
+ } else {
+ collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
+ expandedLayout.load(context, R.xml.media_recommendation_expanded)
+ }
}
}
refreshState()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index bcfceaa..85282a1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -19,8 +19,12 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.os.Bundle;
import android.text.TextUtils;
+import androidx.core.math.MathUtils;
+import androidx.media.utils.MediaConstants;
+
/**
* Utility class with common methods for media controls
*/
@@ -50,4 +54,35 @@
: unknownName);
return applicationName;
}
+
+ /**
+ * Check the bundle for extras indicating the progress percentage
+ *
+ * @param extras
+ * @return the progress value between 0-1 inclusive if prsent, otherwise null
+ */
+ public static Double getDescriptionProgress(Bundle extras) {
+ if (!extras.containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS)) {
+ return null;
+ }
+
+ int status = extras.getInt(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS);
+ switch (status) {
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED:
+ return 0.0;
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED:
+ return 1.0;
+ case MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED: {
+ if (extras
+ .containsKey(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE)) {
+ double percent = extras
+ .getDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE);
+ return MathUtils.clamp(percent, 0.0, 1.0);
+ } else {
+ return 0.5;
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 8d4931a..a689dc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -42,4 +42,20 @@
* [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
*/
fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
+
+ /** Check whether we show explicit indicator on UMO */
+ fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR)
+
+ /**
+ * If true, keep active media controls for the lifetime of the MediaSession, regardless of
+ * whether the underlying notification was dismissed
+ */
+ fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS)
+
+ /** Check whether we show the updated recommendation card. */
+ fun isRecommendationCardUpdateEnabled() =
+ featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
+
+ /** Check whether to get progress information for resume players */
+ fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index bb833df..9ae4577 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -17,8 +17,7 @@
package com.android.systemui.media.dagger;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
-import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
+import com.android.systemui.log.LogBufferFactory;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.media.controls.ui.MediaHost;
@@ -29,12 +28,9 @@
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags;
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
-import com.android.systemui.media.taptotransfer.receiver.ChipReceiverInfo;
-import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
+import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogBuffer;
+import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogBuffer;
import com.android.systemui.plugins.log.LogBuffer;
-import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo;
import java.util.Optional;
@@ -94,22 +90,22 @@
return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager);
}
+ /** Provides a logging buffer related to the media tap-to-transfer chip on the sender device. */
@Provides
@SysUISingleton
- @MediaTttSenderLogger
- static MediaTttLogger<ChipbarInfo> providesMediaTttSenderLogger(
- @MediaTttSenderLogBuffer LogBuffer buffer
- ) {
- return new MediaTttLogger<>("Sender", buffer);
+ @MediaTttSenderLogBuffer
+ static LogBuffer provideMediaTttSenderLogBuffer(LogBufferFactory factory) {
+ return factory.create("MediaTttSender", 30);
}
+ /**
+ * Provides a logging buffer related to the media tap-to-transfer chip on the receiver device.
+ */
@Provides
@SysUISingleton
- @MediaTttReceiverLogger
- static MediaTttLogger<ChipReceiverInfo> providesMediaTttReceiverLogger(
- @MediaTttReceiverLogBuffer LogBuffer buffer
- ) {
- return new MediaTttLogger<>("Receiver", buffer);
+ @MediaTttReceiverLogBuffer
+ static LogBuffer provideMediaTttReceiverLogBuffer(LogBufferFactory factory) {
+ return factory.create("MediaTttReceiver", 20);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 51b5a3d..769e0c8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -16,17 +16,21 @@
package com.android.systemui.media.dialog;
+import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.core.widget.CompoundButtonCompat;
import androidx.recyclerview.widget.RecyclerView;
@@ -186,6 +190,17 @@
mCurrentActivePosition = position;
updateFullItemClickListener(v -> onItemClick(v, device));
setSingleLineLayout(getItemTitle(device));
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+ && mController.isSubStatusSupported() && device.hasDisabledReason()) {
+ //update to subtext with device status
+ setUpDeviceIcon(device);
+ mSubTitleText.setText(
+ Api34Impl.composeDisabledReason(device.getDisableReason(), mContext));
+ updateConnectionFailedStatusIcon();
+ updateFullItemClickListener(null);
+ setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */,
+ false /* showProgressBar */, true /* showSubtitle */,
+ true /* showStatus */);
} else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
setUpDeviceIcon(device);
updateConnectionFailedStatusIcon();
@@ -389,4 +404,12 @@
mTitleText.setText(groupDividerTitle);
}
}
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ @DoNotInline
+ static String composeDisabledReason(int reason, Context context) {
+ return "";
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 4e08050..dc75538 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -45,6 +45,7 @@
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settingslib.Utils;
@@ -142,11 +143,12 @@
final TextView mVolumeValueText;
final ImageView mTitleIcon;
final ProgressBar mProgressBar;
- final MediaOutputSeekbar mSeekBar;
final LinearLayout mTwoLineLayout;
final ImageView mStatusIcon;
final CheckBox mCheckBox;
final ViewGroup mEndTouchArea;
+ @VisibleForTesting
+ MediaOutputSeekbar mSeekBar;
private String mDeviceId;
private ValueAnimator mCornerAnimator;
private ValueAnimator mVolumeAnimator;
@@ -390,6 +392,7 @@
mTitleIcon.setVisibility(View.VISIBLE);
mVolumeValueText.setVisibility(View.GONE);
}
+ mController.logInteractionAdjustVolume(device);
mIsDragging = false;
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index a9e1a4d..4803371 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -95,6 +95,7 @@
private RecyclerView mDevicesRecyclerView;
private LinearLayout mDeviceListLayout;
private LinearLayout mCastAppLayout;
+ private LinearLayout mMediaMetadataSectionLayout;
private Button mDoneButton;
private Button mStopButton;
private Button mAppButton;
@@ -240,6 +241,7 @@
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+ mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section);
mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
mDoneButton = mDialogView.requireViewById(R.id.done);
mStopButton = mDialogView.requireViewById(R.id.stop);
@@ -255,21 +257,17 @@
mDevicesRecyclerView.setLayoutManager(mLayoutManager);
mDevicesRecyclerView.setAdapter(mAdapter);
mDevicesRecyclerView.setHasFixedSize(false);
- // Init header icon
- mHeaderIcon.setOnClickListener(v -> onHeaderIconClick());
// Init bottom buttons
mDoneButton.setOnClickListener(v -> dismiss());
mStopButton.setOnClickListener(v -> {
mMediaOutputController.releaseSession();
dismiss();
});
- mAppButton.setOnClickListener(v -> {
- mBroadcastSender.closeSystemDialogs();
- if (mMediaOutputController.getAppLaunchIntent() != null) {
- mContext.startActivity(mMediaOutputController.getAppLaunchIntent());
- }
- dismiss();
- });
+ mAppButton.setOnClickListener(v -> mMediaOutputController.tryToLaunchMediaApplication());
+ if (mMediaOutputController.isAdvancedLayoutSupported()) {
+ mMediaMetadataSectionLayout.setOnClickListener(
+ v -> mMediaOutputController.tryToLaunchMediaApplication());
+ }
}
@Override
@@ -560,7 +558,7 @@
@Override
public void dismissDialog() {
- dismiss();
+ mBroadcastSender.closeSystemDialogs();
}
void onHeaderIconClick() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 8eb25c4..5f5c686 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -141,7 +141,7 @@
@VisibleForTesting
LocalMediaManager mLocalMediaManager;
@VisibleForTesting
- private MediaOutputMetricLogger mMetricLogger;
+ MediaOutputMetricLogger mMetricLogger;
private int mCurrentState;
private int mColorItemContent;
@@ -382,6 +382,15 @@
return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName);
}
+ void tryToLaunchMediaApplication() {
+ Intent launchIntent = getAppLaunchIntent();
+ if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mCallback.dismissDialog();
+ mContext.startActivity(launchIntent);
+ }
+ }
+
CharSequence getHeaderTitle() {
if (mMediaController != null) {
final MediaMetadata metadata = mMediaController.getMetadata();
@@ -493,20 +502,20 @@
ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors,
isDarkTheme);
if (isDarkTheme) {
- mColorItemContent = mCurrentColorScheme.getAccent1().get(2); // A1-100
- mColorSeekbarProgress = mCurrentColorScheme.getAccent2().get(7); // A2-600
- mColorButtonBackground = mCurrentColorScheme.getAccent1().get(4); // A1-300
- mColorItemBackground = mCurrentColorScheme.getNeutral2().get(9); // N2-800
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().get(9); // A2-800
- mColorPositiveButtonText = mCurrentColorScheme.getAccent2().get(9); // A2-800
- mColorDialogBackground = mCurrentColorScheme.getNeutral1().get(10); // N1-900
+ mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100
+ mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600
+ mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300
+ mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800
+ mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800
+ mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800
+ mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900
} else {
- mColorItemContent = mCurrentColorScheme.getAccent1().get(9); // A1-800
- mColorSeekbarProgress = mCurrentColorScheme.getAccent1().get(4); // A1-300
- mColorButtonBackground = mCurrentColorScheme.getAccent1().get(7); // A1-600
- mColorItemBackground = mCurrentColorScheme.getAccent2().get(1); // A2-50
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().get(2); // A1-100
- mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().get(1); // N1-50
+ mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800
+ mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300
+ mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600
+ mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50
+ mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100
+ mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50
mColorDialogBackground = mCurrentColorScheme.getBackgroundColor();
}
}
@@ -630,50 +639,28 @@
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- //TODO(b/257851968): do the organization only when there's no suggested sorted order
- // we get from application
- attachRangeInfo(devices);
- Collections.sort(devices, Comparator.naturalOrder());
+ if (!isRouteProcessSupported() || (isRouteProcessSupported()
+ && !mLocalMediaManager.isPreferenceRouteListingExist())) {
+ attachRangeInfo(devices);
+ Collections.sort(devices, Comparator.naturalOrder());
+ }
// For the first time building list, to make sure the top device is the connected
// device.
+ boolean needToHandleMutingExpectedDevice =
+ hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote();
+ final MediaDevice connectedMediaDevice =
+ needToHandleMutingExpectedDevice ? null
+ : getCurrentConnectedMediaDevice();
if (mMediaItemList.isEmpty()) {
- boolean needToHandleMutingExpectedDevice =
- hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote();
- final MediaDevice connectedMediaDevice =
- needToHandleMutingExpectedDevice ? null
- : getCurrentConnectedMediaDevice();
if (connectedMediaDevice == null) {
if (DEBUG) {
Log.d(TAG, "No connected media device or muting expected device exist.");
}
- if (needToHandleMutingExpectedDevice) {
- for (MediaDevice device : devices) {
- if (device.isMutingExpectedDevice()) {
- mMediaItemList.add(0, new MediaItem(device));
- mMediaItemList.add(1, new MediaItem(mContext.getString(
- R.string.media_output_group_title_speakers_and_displays),
- MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
- } else {
- mMediaItemList.add(new MediaItem(device));
- }
- }
- mMediaItemList.add(new MediaItem());
- } else {
- mMediaItemList.addAll(
- devices.stream().map(MediaItem::new).collect(Collectors.toList()));
- categorizeMediaItems(null);
- }
+ categorizeMediaItems(null, devices, needToHandleMutingExpectedDevice);
return;
}
// selected device exist
- for (MediaDevice device : devices) {
- if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
- mMediaItemList.add(0, new MediaItem(device));
- } else {
- mMediaItemList.add(new MediaItem(device));
- }
- }
- categorizeMediaItems(connectedMediaDevice);
+ categorizeMediaItems(connectedMediaDevice, devices, false);
return;
}
// To keep the same list order
@@ -707,31 +694,46 @@
}
}
- private void categorizeMediaItems(MediaDevice connectedMediaDevice) {
+ private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices,
+ boolean needToHandleMutingExpectedDevice) {
synchronized (mMediaDevicesLock) {
Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
MediaDevice::getId).collect(Collectors.toSet());
if (connectedMediaDevice != null) {
selectedDevicesIds.add(connectedMediaDevice.getId());
}
- int latestSelected = 1;
- for (MediaItem item : mMediaItemList) {
- if (item.getMediaDevice().isPresent()) {
- MediaDevice device = item.getMediaDevice().get();
- if (selectedDevicesIds.contains(device.getId())) {
- latestSelected = mMediaItemList.indexOf(item) + 1;
- } else {
- mMediaItemList.add(latestSelected, new MediaItem(mContext.getString(
- R.string.media_output_group_title_speakers_and_displays),
- MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
- break;
+ boolean suggestedDeviceAdded = false;
+ boolean displayGroupAdded = false;
+ for (MediaDevice device : devices) {
+ if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) {
+ mMediaItemList.add(0, new MediaItem(device));
+ } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains(
+ device.getId())) {
+ mMediaItemList.add(0, new MediaItem(device));
+ } else {
+ if (device.isSuggestedDevice() && !suggestedDeviceAdded) {
+ attachGroupDivider(mContext.getString(
+ R.string.media_output_group_title_suggested_device));
+ suggestedDeviceAdded = true;
+ } else if (!device.isSuggestedDevice() && !displayGroupAdded) {
+ attachGroupDivider(mContext.getString(
+ R.string.media_output_group_title_speakers_and_displays));
+ displayGroupAdded = true;
}
+ mMediaItemList.add(new MediaItem(device));
}
}
mMediaItemList.add(new MediaItem());
}
}
+ private void attachGroupDivider(String title) {
+ synchronized (mMediaDevicesLock) {
+ mMediaItemList.add(
+ new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
+ }
+ }
+
private void attachRangeInfo(List<MediaDevice> devices) {
for (MediaDevice mediaDevice : devices) {
if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) {
@@ -751,6 +753,14 @@
return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT);
}
+ public boolean isRouteProcessSupported() {
+ return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING);
+ }
+
+ public boolean isSubStatusSupported() {
+ return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_DEVICE_STATUS);
+ }
+
List<MediaDevice> getGroupMediaDevices() {
final List<MediaDevice> selectedDevices = getSelectedMediaDevice();
final List<MediaDevice> selectableDevices = getSelectableMediaDevice();
@@ -860,12 +870,15 @@
}
void adjustVolume(MediaDevice device, int volume) {
- mMetricLogger.logInteractionAdjustVolume(device);
ThreadUtils.postOnBackgroundThread(() -> {
device.requestSetVolume(volume);
});
}
+ void logInteractionAdjustVolume(MediaDevice device) {
+ mMetricLogger.logInteractionAdjustVolume(device);
+ }
+
String getPackageName() {
return mPackageName;
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
new file mode 100644
index 0000000..e35575b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
@@ -0,0 +1,71 @@
+/*
+ * 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.media.dialog;
+
+import android.annotation.MainThread;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.statusbar.CommandQueue;
+
+import javax.inject.Inject;
+
+/** Controls display of media output switcher. */
+@SysUISingleton
+public class MediaOutputSwitcherDialogUI implements CoreStartable, CommandQueue.Callbacks {
+
+ private static final String TAG = "MediaOutputSwitcherDialogUI";
+
+ private final CommandQueue mCommandQueue;
+ private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+ private final FeatureFlags mFeatureFlags;
+
+ @Inject
+ public MediaOutputSwitcherDialogUI(
+ Context context,
+ CommandQueue commandQueue,
+ MediaOutputDialogFactory mediaOutputDialogFactory,
+ FeatureFlags featureFlags) {
+ mCommandQueue = commandQueue;
+ mMediaOutputDialogFactory = mediaOutputDialogFactory;
+ mFeatureFlags = featureFlags;
+ }
+
+ @Override
+ public void start() {
+ if (mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_SHOW_API_ENABLED)) {
+ mCommandQueue.addCallback(this);
+ } else {
+ Log.w(TAG, "Show media output switcher is not enabled.");
+ }
+ }
+
+ @Override
+ @MainThread
+ public void showMediaOutputSwitcher(String packageName) {
+ if (!TextUtils.isEmpty(packageName)) {
+ mMediaOutputDialogFactory.create(packageName, false, null);
+ } else {
+ Log.e(TAG, "Unable to launch media output dialog. Package name is empty.");
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 8a565fa..60504e4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -30,4 +30,8 @@
/** Check whether the flag for the receiver success state is enabled. */
fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
+
+ /** True if the media transfer chip can be dismissed via a gesture. */
+ fun isMediaTttDismissGestureEnabled(): Boolean =
+ featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
deleted file mode 100644
index 8aef938..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media.taptotransfer.common
-
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
-import com.android.systemui.temporarydisplay.TemporaryViewLogger
-
-/**
- * A logger for media tap-to-transfer events.
- *
- * @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver".
- *
- * TODO(b/245610654): We should de-couple the sender and receiver loggers, since they're vastly
- * different experiences.
- */
-class MediaTttLogger<T : TemporaryViewInfo>(
- deviceTypeTag: String,
- buffer: LogBuffer
-) : TemporaryViewLogger<T>(buffer, BASE_TAG + deviceTypeTag) {
- /** Logs a change in the chip state for the given [mediaRouteId]. */
- fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
- buffer.log(
- tag,
- LogLevel.DEBUG,
- {
- str1 = stateName
- str2 = mediaRouteId
- str3 = packageName
- },
- { "State changed to $str1 for ID=$str2 package=$str3" }
- )
- }
-
- /**
- * Logs an error in trying to update to [displayState].
- *
- * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or
- * a [android.app.StatusBarManager.MediaTransferReceiverState].
- */
- fun logStateChangeError(displayState: Int) {
- buffer.log(
- tag,
- LogLevel.ERROR,
- { int1 = displayState },
- { "Cannot display state=$int1; aborting" }
- )
- }
-
- /**
- * Logs an invalid sender state transition error in trying to update to [desiredState].
- *
- * @param currentState the previous state of the chip.
- * @param desiredState the new state of the chip.
- */
- fun logInvalidStateTransitionError(
- currentState: String,
- desiredState: String
- ) {
- buffer.log(
- tag,
- LogLevel.ERROR,
- {
- str1 = currentState
- str2 = desiredState
- },
- { "Cannot display state=$str2 after state=$str1; invalid transition" }
- )
- }
-
- /** Logs that we couldn't find information for [packageName]. */
- fun logPackageNotFound(packageName: String) {
- buffer.log(
- tag,
- LogLevel.DEBUG,
- { str1 = packageName },
- { "Package $str1 could not be found" }
- )
- }
-
- /**
- * Logs that a removal request has been bypassed (ignored).
- *
- * @param removalReason the reason that the chip removal was requested.
- * @param bypassReason the reason that the request was bypassed.
- */
- fun logRemovalBypass(removalReason: String, bypassReason: String) {
- buffer.log(
- tag,
- LogLevel.DEBUG,
- {
- str1 = removalReason
- str2 = bypassReason
- },
- { "Chip removal requested due to $str1; however, removal was ignored because $str2" })
- }
-}
-
-private const val BASE_TAG = "MediaTtt"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt
new file mode 100644
index 0000000..0e839c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtils.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.taptotransfer.common
+
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+
+/** A helper for logging media tap-to-transfer events. */
+object MediaTttLoggerUtils {
+ fun logStateChange(
+ buffer: LogBuffer,
+ tag: String,
+ stateName: String,
+ mediaRouteId: String,
+ packageName: String?,
+ ) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ {
+ str1 = stateName
+ str2 = mediaRouteId
+ str3 = packageName
+ },
+ { "State changed to $str1 for ID=$str2 package=$str3" }
+ )
+ }
+
+ fun logStateChangeError(buffer: LogBuffer, tag: String, displayState: Int) {
+ buffer.log(
+ tag,
+ LogLevel.ERROR,
+ { int1 = displayState },
+ { "Cannot display state=$int1; aborting" }
+ )
+ }
+
+ fun logPackageNotFound(buffer: LogBuffer, tag: String, packageName: String) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ { str1 = packageName },
+ { "Package $str1 could not be found" }
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 066c185..a3ae943 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -25,7 +25,6 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.TintedIcon
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
/** Utility methods for media tap-to-transfer. */
class MediaTttUtils {
@@ -43,12 +42,13 @@
* default name and icon if we can't find the app name/icon.
*
* @param appPackageName the package name of the app playing the media.
- * @param logger the logger to use for any errors.
+ * @param onPackageNotFoundException a function run if a
+ * [PackageManager.NameNotFoundException] occurs.
*/
fun getIconInfoFromPackageName(
context: Context,
appPackageName: String?,
- logger: MediaTttLogger<out TemporaryViewInfo>
+ onPackageNotFoundException: () -> Unit,
): IconInfo {
if (appPackageName != null) {
val packageManager = context.packageManager
@@ -70,7 +70,7 @@
isAppIcon = true
)
} catch (e: PackageManager.NameNotFoundException) {
- logger.logPackageNotFound(appPackageName)
+ onPackageNotFoundException.invoke()
}
}
return IconInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 889147b..34bf74fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -30,8 +30,8 @@
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
+import android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
import com.android.internal.widget.CachingIconView
-import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.ui.binder.TintedIconViewBinder
@@ -40,7 +40,6 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttIcon
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -65,7 +64,7 @@
open class MediaTttChipControllerReceiver @Inject constructor(
private val commandQueue: CommandQueue,
context: Context,
- @MediaTttReceiverLogger logger: MediaTttLogger<ChipReceiverInfo>,
+ logger: MediaTttReceiverLogger,
windowManager: WindowManager,
mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
@@ -78,7 +77,8 @@
private val viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
-) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>(
+ private val rippleController: MediaTttReceiverRippleController,
+) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttReceiverLogger>(
context,
logger,
windowManager,
@@ -114,9 +114,6 @@
}
}
- private var maxRippleWidth: Float = 0f
- private var maxRippleHeight: Float = 0f
-
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info,
@@ -175,9 +172,10 @@
}
override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) {
- var iconInfo = MediaTttUtils.getIconInfoFromPackageName(
- context, newInfo.routeInfo.clientPackageName, logger
- )
+ val packageName = newInfo.routeInfo.clientPackageName
+ var iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, packageName) {
+ logger.logPackageNotFound(packageName)
+ }
if (newInfo.appNameOverride != null) {
iconInfo = iconInfo.copy(
@@ -201,41 +199,44 @@
val iconView = currentView.getAppIconView()
iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
+ iconView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
TintedIconViewBinder.bind(iconInfo.toTintedIcon(), iconView)
}
override fun animateViewIn(view: ViewGroup) {
val appIconView = view.getAppIconView()
- appIconView.animate()
- .translationYBy(-1 * getTranslationAmount().toFloat())
- .setDuration(ICON_TRANSLATION_ANIM_DURATION)
- .start()
- appIconView.animate()
- .alpha(1f)
- .setDuration(ICON_ALPHA_ANIM_DURATION)
- .start()
- // Using withEndAction{} doesn't apply a11y focus when screen is unlocked.
- appIconView.postOnAnimation { view.requestAccessibilityFocus() }
- expandRipple(view.requireViewById(R.id.ripple))
+ val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
+ val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+ animateViewTranslationAndFade(appIconView, -1 * getTranslationAmount(), 1f)
+ animateViewTranslationAndFade(iconRippleView, -1 * getTranslationAmount(), 1f)
+ rippleController.expandToInProgressState(rippleView, iconRippleView)
}
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val appIconView = view.getAppIconView()
- appIconView.animate()
- .translationYBy(getTranslationAmount().toFloat())
- .setDuration(ICON_TRANSLATION_ANIM_DURATION)
- .start()
- appIconView.animate()
- .alpha(0f)
- .setDuration(ICON_ALPHA_ANIM_DURATION)
- .start()
-
+ val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
- expandRippleToFull(rippleView, onAnimationEnd)
+ rippleController.expandToSuccessState(rippleView, onAnimationEnd)
+ animateViewTranslationAndFade(
+ iconRippleView,
+ -1 * getTranslationAmount(),
+ 0f,
+ translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
+ alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
+ )
+ animateViewTranslationAndFade(
+ appIconView,
+ -1 * getTranslationAmount(),
+ 0f,
+ translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
+ alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
+ )
} else {
- rippleView.collapseRipple(onAnimationEnd)
+ rippleController.collapseRipple(rippleView, onAnimationEnd)
+ animateViewTranslationAndFade(iconRippleView, getTranslationAmount(), 0f)
+ animateViewTranslationAndFade(appIconView, getTranslationAmount(), 0f)
}
}
@@ -245,74 +246,41 @@
viewUtil.setRectToViewWindowLocation(view.getAppIconView(), outRect)
}
+ /** Animation of view translation and fading. */
+ private fun animateViewTranslationAndFade(
+ view: View,
+ translationYBy: Float,
+ alphaEndValue: Float,
+ translationDuration: Long = ICON_TRANSLATION_ANIM_DURATION,
+ alphaDuration: Long = ICON_ALPHA_ANIM_DURATION,
+ ) {
+ view.animate()
+ .translationYBy(translationYBy)
+ .setDuration(translationDuration)
+ .start()
+ view.animate()
+ .alpha(alphaEndValue)
+ .setDuration(alphaDuration)
+ .start()
+ }
+
/** Returns the amount that the chip will be translated by in its intro animation. */
- private fun getTranslationAmount(): Int {
- return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
- }
-
- private fun expandRipple(rippleView: ReceiverChipRippleView) {
- if (rippleView.rippleInProgress()) {
- // Skip if ripple is still playing
- return
- }
-
- // In case the device orientation changes, we need to reset the layout.
- rippleView.addOnLayoutChangeListener (
- View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
- if (v == null) return@OnLayoutChangeListener
-
- val layoutChangedRippleView = v as ReceiverChipRippleView
- layoutRipple(layoutChangedRippleView)
- layoutChangedRippleView.invalidate()
- }
- )
- rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
- override fun onViewDetachedFromWindow(view: View?) {}
-
- override fun onViewAttachedToWindow(view: View?) {
- if (view == null) {
- return
- }
- val attachedRippleView = view as ReceiverChipRippleView
- layoutRipple(attachedRippleView)
- attachedRippleView.expandRipple()
- attachedRippleView.removeOnAttachStateChangeListener(this)
- }
- })
- }
-
- private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
- val windowBounds = windowManager.currentWindowMetrics.bounds
- val height = windowBounds.height().toFloat()
- val width = windowBounds.width().toFloat()
-
- if (isFullScreen) {
- maxRippleHeight = height * 2f
- maxRippleWidth = width * 2f
- } else {
- maxRippleHeight = height / 2f
- maxRippleWidth = width / 2f
- }
- rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
- // Center the ripple on the bottom of the screen in the middle.
- rippleView.setCenter(width * 0.5f, height)
- val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
- rippleView.setColor(color, 70)
+ private fun getTranslationAmount(): Float {
+ return rippleController.getRippleSize() * 0.5f -
+ rippleController.getReceiverIconSize()
}
private fun View.getAppIconView(): CachingIconView {
return this.requireViewById(R.id.app_icon)
}
- private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
- layoutRipple(rippleView, true)
- rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+ companion object {
+ private const val ICON_TRANSLATION_ANIM_DURATION = 500L
+ private const val ICON_TRANSLATION_SUCCEEDED_DURATION = 167L
+ private val ICON_ALPHA_ANIM_DURATION = 5.frames
}
}
-val ICON_TRANSLATION_ANIM_DURATION = 30.frames
-val ICON_ALPHA_ANIM_DURATION = 5.frames
-
data class ChipReceiverInfo(
val routeInfo: MediaRoute2Info,
val appIconDrawableOverride: Drawable?,
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogBuffer.java
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
rename to packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogBuffer.java
index 1570d43..67e464c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogBuffer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.log.dagger;
+package com.android.systemui.media.taptotransfer.receiver;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -26,8 +26,7 @@
import javax.inject.Qualifier;
/**
- * A {@link LogBuffer} for
- * {@link com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger}.
+ * A {@link LogBuffer} for receiver logs.
*/
@Qualifier
@Documented
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
index 54fc48d..b0c6257 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
@@ -13,14 +13,44 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.systemui.media.taptotransfer.receiver
-import java.lang.annotation.Documented
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
-import javax.inject.Qualifier
+import android.app.StatusBarManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.taptotransfer.common.MediaTttLoggerUtils
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.temporarydisplay.TemporaryViewLogger
+import javax.inject.Inject
-@Qualifier
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-annotation class MediaTttReceiverLogger
+/** A logger for all events related to the media tap-to-transfer receiver experience. */
+@SysUISingleton
+class MediaTttReceiverLogger
+@Inject
+constructor(
+ @MediaTttReceiverLogBuffer buffer: LogBuffer,
+) : TemporaryViewLogger<ChipReceiverInfo>(buffer, TAG) {
+
+ /** Logs a change in the chip state for the given [mediaRouteId]. */
+ fun logStateChange(
+ stateName: String,
+ mediaRouteId: String,
+ packageName: String?,
+ ) {
+ MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, mediaRouteId, packageName)
+ }
+
+ /** Logs an error in trying to update to [displayState]. */
+ fun logStateChangeError(@StatusBarManager.MediaTransferReceiverState displayState: Int) {
+ MediaTttLoggerUtils.logStateChangeError(buffer, TAG, displayState)
+ }
+
+ /** Logs that we couldn't find information for [packageName]. */
+ fun logPackageNotFound(packageName: String) {
+ MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
+ }
+
+ companion object {
+ private const val TAG = "MediaTttReceiver"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
new file mode 100644
index 0000000..5013802
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.media.taptotransfer.receiver
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import android.view.WindowManager
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import javax.inject.Inject
+
+/**
+ * A controller responsible for the animation of the ripples shown in media tap-to-transfer on the
+ * receiving device.
+ */
+class MediaTttReceiverRippleController
+@Inject
+constructor(
+ private val context: Context,
+ private val windowManager: WindowManager,
+) {
+
+ private var maxRippleWidth: Float = 0f
+ private var maxRippleHeight: Float = 0f
+
+ /** Expands the icon and main ripple to in-progress state */
+ fun expandToInProgressState(
+ mainRippleView: ReceiverChipRippleView,
+ iconRippleView: ReceiverChipRippleView,
+ ) {
+ expandRipple(mainRippleView, isIconRipple = false)
+ expandRipple(iconRippleView, isIconRipple = true)
+ }
+
+ private fun expandRipple(rippleView: ReceiverChipRippleView, isIconRipple: Boolean) {
+ if (rippleView.rippleInProgress()) {
+ // Skip if ripple is still playing
+ return
+ }
+
+ // In case the device orientation changes, we need to reset the layout.
+ rippleView.addOnLayoutChangeListener(
+ View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
+ if (v == null) return@OnLayoutChangeListener
+
+ val layoutChangedRippleView = v as ReceiverChipRippleView
+ if (isIconRipple) {
+ layoutIconRipple(layoutChangedRippleView)
+ } else {
+ layoutRipple(layoutChangedRippleView)
+ }
+ layoutChangedRippleView.invalidate()
+ }
+ )
+ rippleView.addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewDetachedFromWindow(view: View?) {}
+
+ override fun onViewAttachedToWindow(view: View?) {
+ if (view == null) {
+ return
+ }
+ val attachedRippleView = view as ReceiverChipRippleView
+ if (isIconRipple) {
+ layoutIconRipple(attachedRippleView)
+ } else {
+ layoutRipple(attachedRippleView)
+ }
+ attachedRippleView.expandRipple()
+ attachedRippleView.removeOnAttachStateChangeListener(this)
+ }
+ }
+ )
+ }
+
+ /** Expands the ripple to cover the screen. */
+ fun expandToSuccessState(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+ layoutRipple(rippleView, isFullScreen = true)
+ rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+ }
+
+ /** Collapses the ripple. */
+ fun collapseRipple(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable? = null) {
+ rippleView.collapseRipple(onAnimationEnd)
+ }
+
+ private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
+ val windowBounds = windowManager.currentWindowMetrics.bounds
+ val height = windowBounds.height().toFloat()
+ val width = windowBounds.width().toFloat()
+
+ if (isFullScreen) {
+ maxRippleHeight = height * 2f
+ maxRippleWidth = width * 2f
+ } else {
+ maxRippleHeight = getRippleSize()
+ maxRippleWidth = getRippleSize()
+ }
+ rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
+ // Center the ripple on the bottom of the screen in the middle.
+ rippleView.setCenter(width * 0.5f, height)
+ rippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
+ }
+
+ private fun layoutIconRipple(iconRippleView: ReceiverChipRippleView) {
+ val windowBounds = windowManager.currentWindowMetrics.bounds
+ val height = windowBounds.height().toFloat()
+ val width = windowBounds.width().toFloat()
+ val radius = getReceiverIconSize().toFloat()
+
+ iconRippleView.setMaxSize(radius * 0.8f, radius * 0.8f)
+ iconRippleView.setCenter(
+ width * 0.5f,
+ height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin()
+ )
+ iconRippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
+ }
+
+ private fun getRippleColor(): Int {
+ var colorStateList =
+ ColorStateList.valueOf(
+ Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
+ )
+ return colorStateList.withLStar(TONE_PERCENT).defaultColor
+ }
+
+ /** Returns the size of the ripple. */
+ internal fun getRippleSize(): Float {
+ return getReceiverIconSize() * 4f
+ }
+
+ /** Returns the size of the icon of the receiver. */
+ internal fun getReceiverIconSize(): Int {
+ return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver)
+ }
+
+ /** Return the bottom margin of the icon of the receiver. */
+ internal fun getReceiverIconBottomMargin(): Int {
+ // Adding a margin to make sure ripple behind the icon is not cut by the screen bounds.
+ return context.resources.getDimensionPixelSize(
+ R.dimen.media_ttt_receiver_icon_bottom_margin
+ )
+ }
+
+ companion object {
+ const val RIPPLE_OPACITY = 70
+ const val TONE_PERCENT = 95f
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 87b2528..f1acae8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -33,14 +33,14 @@
private var isStarted: Boolean
init {
- setupShader(RippleShader.RippleShape.ELLIPSE)
+ setupShader(RippleShader.RippleShape.CIRCLE)
setRippleFill(true)
setSparkleStrength(0f)
- duration = 3000L
isStarted = false
}
fun expandRipple(onAnimationEnd: Runnable? = null) {
+ duration = DEFAULT_DURATION
isStarted = true
super.startRipple(onAnimationEnd)
}
@@ -50,6 +50,7 @@
if (!isStarted) {
return // Ignore if ripple is not started yet.
}
+ duration = DEFAULT_DURATION
// Reset all listeners to animator.
animator.removeAllListeners()
animator.addListener(object : AnimatorListenerAdapter() {
@@ -74,12 +75,13 @@
setRippleFill(false)
val startingPercentage = calculateStartingPercentage(newHeight)
+ animator.duration = EXPAND_TO_FULL_DURATION
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
- rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
- rippleShader.distortionStrength = 1 - rippleShader.progress
- rippleShader.pixelDensity = 1 - rippleShader.progress
+ rippleShader.rawProgress = startingPercentage + (progress * (1 - startingPercentage))
+ rippleShader.distortionStrength = 1 - rippleShader.rawProgress
+ rippleShader.pixelDensity = 1 - rippleShader.rawProgress
rippleShader.time = now.toFloat()
invalidate()
}
@@ -100,4 +102,9 @@
val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
return 1 - remainingPercentage
}
+
+ companion object {
+ const val DEFAULT_DURATION = 333L
+ const val EXPAND_TO_FULL_DURATION = 1000L
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index 9f44d98..89ca5d3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -23,17 +23,20 @@
import com.android.internal.logging.UiEventLogger
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.CoreStartable
+import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
+import java.io.PrintWriter
import javax.inject.Inject
/**
@@ -47,12 +50,12 @@
private val chipbarCoordinator: ChipbarCoordinator,
private val commandQueue: CommandQueue,
private val context: Context,
- @MediaTttSenderLogger private val logger: MediaTttLogger<ChipbarInfo>,
+ private val dumpManager: DumpManager,
+ private val logger: MediaTttSenderLogger,
private val mediaTttFlags: MediaTttFlags,
private val uiEventLogger: MediaTttSenderUiEventLogger,
-) : CoreStartable {
+) : CoreStartable, Dumpable {
- private var displayedState: ChipStateSender? = null
// A map to store current chip state per id.
private var stateMap: MutableMap<String, ChipStateSender> = mutableMapOf()
@@ -74,6 +77,7 @@
override fun start() {
if (mediaTttFlags.isMediaTttEnabled()) {
commandQueue.addCallback(commandQueueCallbacks)
+ dumpManager.registerNormalDumpable(this)
}
}
@@ -91,42 +95,42 @@
return
}
- val currentState = stateMap[routeInfo.id]
- if (!ChipStateSender.isValidStateTransition(currentState, chipState)) {
+ val currentStateForId: ChipStateSender? = stateMap[routeInfo.id]
+ if (!ChipStateSender.isValidStateTransition(currentStateForId, chipState)) {
// ChipStateSender.FAR_FROM_RECEIVER is the default state when there is no state.
logger.logInvalidStateTransitionError(
- currentState = currentState?.name ?: ChipStateSender.FAR_FROM_RECEIVER.name,
+ currentState = currentStateForId?.name ?: ChipStateSender.FAR_FROM_RECEIVER.name,
chipState.name
)
return
}
uiEventLogger.logSenderStateChange(chipState)
- stateMap.put(routeInfo.id, chipState)
if (chipState == ChipStateSender.FAR_FROM_RECEIVER) {
- // No need to store the state since it is the default state
- stateMap.remove(routeInfo.id)
- // Return early if we're not displaying a chip anyway
- val currentDisplayedState = displayedState ?: return
+ // Return early if we're not displaying a chip for this ID anyway
+ if (currentStateForId == null) return
val removalReason = ChipStateSender.FAR_FROM_RECEIVER.name
if (
- currentDisplayedState.transferStatus == TransferStatus.IN_PROGRESS ||
- currentDisplayedState.transferStatus == TransferStatus.SUCCEEDED
+ currentStateForId.transferStatus == TransferStatus.IN_PROGRESS ||
+ currentStateForId.transferStatus == TransferStatus.SUCCEEDED
) {
// Don't remove the chip if we're in progress or succeeded, since the user should
// still be able to see the status of the transfer.
logger.logRemovalBypass(
removalReason,
- bypassReason = "transferStatus=${currentDisplayedState.transferStatus.name}"
+ bypassReason = "transferStatus=${currentStateForId.transferStatus.name}"
)
return
}
- displayedState = null
+ // No need to store the state since it is the default state
+ removeIdFromStore(routeInfo.id, reason = removalReason)
chipbarCoordinator.removeView(routeInfo.id, removalReason)
} else {
- displayedState = chipState
+ stateMap[routeInfo.id] = chipState
+ logger.logStateMap(stateMap)
+ chipbarCoordinator.registerListener(displayListener)
chipbarCoordinator.displayView(
createChipbarInfo(
chipState,
@@ -135,7 +139,7 @@
context,
logger,
)
- ) { stateMap.remove(routeInfo.id) }
+ )
}
}
@@ -147,16 +151,23 @@
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
context: Context,
- logger: MediaTttLogger<ChipbarInfo>,
+ logger: MediaTttSenderLogger,
): ChipbarInfo {
val packageName = routeInfo.clientPackageName
- val otherDeviceName = routeInfo.name.toString()
+ val otherDeviceName =
+ if (routeInfo.name.isBlank()) {
+ context.getString(R.string.media_ttt_default_device_type)
+ } else {
+ routeInfo.name.toString()
+ }
+ val icon =
+ MediaTttUtils.getIconInfoFromPackageName(context, packageName) {
+ logger.logPackageNotFound(packageName)
+ }
return ChipbarInfo(
// Display the app's icon as the start icon
- startIcon =
- MediaTttUtils.getIconInfoFromPackageName(context, packageName, logger)
- .toTintedIcon(),
+ startIcon = icon.toTintedIcon(),
text = chipStateSender.getChipTextString(context, otherDeviceName),
endItem =
when (chipStateSender.endItem) {
@@ -177,6 +188,7 @@
}
},
vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+ allowSwipeToDismiss = true,
windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
timeoutMs = chipStateSender.timeout,
@@ -220,4 +232,21 @@
onClickListener,
)
}
+
+ private val displayListener =
+ TemporaryViewDisplayController.Listener { id, reason -> removeIdFromStore(id, reason) }
+
+ private fun removeIdFromStore(id: String, reason: String) {
+ logger.logStateMapRemoval(id, reason)
+ stateMap.remove(id)
+ logger.logStateMap(stateMap)
+ if (stateMap.isEmpty()) {
+ chipbarCoordinator.unregisterListener(displayListener)
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("Current sender states:")
+ pw.println(stateMap.toString())
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogBuffer.java
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
rename to packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogBuffer.java
index bf216c6..a262e97 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogBuffer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.log.dagger;
+package com.android.systemui.media.taptotransfer.sender;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -26,8 +26,7 @@
import javax.inject.Qualifier;
/**
- * A {@link LogBuffer} for
- * {@link com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger}.
+ * A {@link LogBuffer} for sender logs.
*/
@Qualifier
@Documented
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
index 4393af9..964a95b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt
@@ -13,14 +13,102 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.systemui.media.taptotransfer.sender
-import java.lang.annotation.Documented
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
-import javax.inject.Qualifier
+import android.app.StatusBarManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.taptotransfer.common.MediaTttLoggerUtils
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
-@Qualifier
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-annotation class MediaTttSenderLogger
+/** A logger for all events related to the media tap-to-transfer sender experience. */
+@SysUISingleton
+class MediaTttSenderLogger
+@Inject
+constructor(
+ @MediaTttSenderLogBuffer private val buffer: LogBuffer,
+) {
+ /** Logs a change in the chip state for the given [mediaRouteId]. */
+ fun logStateChange(
+ stateName: String,
+ mediaRouteId: String,
+ packageName: String?,
+ ) {
+ MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, mediaRouteId, packageName)
+ }
+
+ /** Logs an error in trying to update to [displayState]. */
+ fun logStateChangeError(@StatusBarManager.MediaTransferSenderState displayState: Int) {
+ MediaTttLoggerUtils.logStateChangeError(buffer, TAG, displayState)
+ }
+
+ /** Logs that we couldn't find information for [packageName]. */
+ fun logPackageNotFound(packageName: String) {
+ MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
+ }
+
+ /**
+ * Logs an invalid sender state transition error in trying to update to [desiredState].
+ *
+ * @param currentState the previous state of the chip.
+ * @param desiredState the new state of the chip.
+ */
+ fun logInvalidStateTransitionError(currentState: String, desiredState: String) {
+ buffer.log(
+ TAG,
+ LogLevel.ERROR,
+ {
+ str1 = currentState
+ str2 = desiredState
+ },
+ { "Cannot display state=$str2 after state=$str1; invalid transition" }
+ )
+ }
+
+ /**
+ * Logs that a removal request has been bypassed (ignored).
+ *
+ * @param removalReason the reason that the chip removal was requested.
+ * @param bypassReason the reason that the request was bypassed.
+ */
+ fun logRemovalBypass(removalReason: String, bypassReason: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = removalReason
+ str2 = bypassReason
+ },
+ { "Chip removal requested due to $str1; however, removal was ignored because $str2" }
+ )
+ }
+
+ /** Logs the current contents of the state map. */
+ fun logStateMap(map: Map<String, ChipStateSender>) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { str1 = map.toString() },
+ { "Current sender states: $str1" }
+ )
+ }
+
+ /** Logs that [id] has been removed from the state map due to [reason]. */
+ fun logStateMapRemoval(id: String, reason: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = id
+ str2 = reason
+ },
+ { "State removal: id=$str1 reason=$str2" }
+ )
+ }
+
+ companion object {
+ private const val TAG = "MediaTttSender"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index 6c41caa..e665d83 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -19,17 +19,23 @@
import android.app.Activity
import android.content.ComponentName
import android.content.Context
+import android.os.UserHandle
import com.android.launcher3.icons.IconFactory
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.MediaProjectionAppSelectorActivity
+import com.android.systemui.media.MediaProjectionAppSelectorActivity.Companion.EXTRA_HOST_APP_USER_HANDLE
+import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskLabelLoader
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider
import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider
+import com.android.systemui.mediaprojection.devicepolicy.MediaProjectionDevicePolicyModule
+import com.android.systemui.mediaprojection.devicepolicy.PersonalProfile
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.policy.ConfigurationController
import dagger.Binds
@@ -46,9 +52,14 @@
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class HostUserHandle
+
@Retention(AnnotationRetention.RUNTIME) @Scope annotation class MediaProjectionAppSelectorScope
-@Module(subcomponents = [MediaProjectionAppSelectorComponent::class])
+@Module(
+ subcomponents = [MediaProjectionAppSelectorComponent::class],
+ includes = [MediaProjectionDevicePolicyModule::class]
+)
interface MediaProjectionModule {
@Binds
@IntoMap
@@ -58,9 +69,10 @@
): Activity
}
-/** Scoped values for [MediaProjectionAppSelectorComponent].
- * We create a scope for the activity so certain dependencies like [TaskPreviewSizeProvider]
- * could be reused. */
+/**
+ * Scoped values for [MediaProjectionAppSelectorComponent]. We create a scope for the activity so
+ * certain dependencies like [TaskPreviewSizeProvider] could be reused.
+ */
@Module
interface MediaProjectionAppSelectorModule {
@@ -72,6 +84,10 @@
@Binds
@MediaProjectionAppSelectorScope
+ fun bindRecentTaskLabelLoader(impl: ActivityTaskManagerLabelLoader): RecentTaskLabelLoader
+
+ @Binds
+ @MediaProjectionAppSelectorScope
fun bindRecentTaskListProvider(impl: ShellRecentTaskListProvider): RecentTaskListProvider
@Binds
@@ -83,7 +99,7 @@
@MediaProjectionAppSelector
@MediaProjectionAppSelectorScope
fun provideAppSelectorComponentName(context: Context): ComponentName =
- ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
+ ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
@Provides
@MediaProjectionAppSelector
@@ -93,9 +109,20 @@
): ConfigurationController = ConfigurationControllerImpl(activity)
@Provides
- fun bindIconFactory(
- context: Context
- ): IconFactory = IconFactory.obtain(context)
+ @HostUserHandle
+ @MediaProjectionAppSelectorScope
+ fun hostUserHandle(activity: MediaProjectionAppSelectorActivity): UserHandle {
+ val extras =
+ activity.intent.extras
+ ?: error("MediaProjectionAppSelectorActivity should be launched with extras")
+ return extras.getParcelable(EXTRA_HOST_APP_USER_HANDLE)
+ ?: error(
+ "MediaProjectionAppSelectorActivity should be provided with " +
+ "$EXTRA_HOST_APP_USER_HANDLE extra"
+ )
+ }
+
+ @Provides fun bindIconFactory(context: Context): IconFactory = IconFactory.obtain(context)
@Provides
@MediaProjectionAppSelector
@@ -112,9 +139,7 @@
/** Generates [MediaProjectionAppSelectorComponent]. */
@Subcomponent.Factory
interface Factory {
- /**
- * Create a factory to inject the activity into the graph
- */
+ /** Create a factory to inject the activity into the graph */
fun create(
@BindsInstance activity: MediaProjectionAppSelectorActivity,
@BindsInstance view: MediaProjectionAppSelectorView,
@@ -124,6 +149,8 @@
val controller: MediaProjectionAppSelectorController
val recentsViewController: MediaProjectionRecentsViewController
+ @get:HostUserHandle val hostUserHandle: UserHandle
+ @get:PersonalProfile val personalProfileUserHandle: UserHandle
@MediaProjectionAppSelector val configurationController: ConfigurationController
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
index d744a40b..52c7ca3 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
@@ -17,24 +17,36 @@
package com.android.systemui.mediaprojection.appselector
import android.content.ComponentName
+import android.os.UserHandle
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
-import javax.inject.Inject
@MediaProjectionAppSelectorScope
-class MediaProjectionAppSelectorController @Inject constructor(
+class MediaProjectionAppSelectorController
+@Inject
+constructor(
private val recentTaskListProvider: RecentTaskListProvider,
private val view: MediaProjectionAppSelectorView,
+ private val flags: FeatureFlags,
+ @HostUserHandle private val hostUserHandle: UserHandle,
@MediaProjectionAppSelector private val scope: CoroutineScope,
@MediaProjectionAppSelector private val appSelectorComponentName: ComponentName
) {
fun init() {
scope.launch {
- val tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
+ val recentTasks = recentTaskListProvider.loadRecentTasks()
+
+ val tasks = recentTasks
+ .filterDevicePolicyRestrictedTasks()
+ .sortedTasks()
+
view.bind(tasks)
}
}
@@ -43,9 +55,20 @@
scope.cancel()
}
- private fun List<RecentTask>.sortTasks(): List<RecentTask> =
- sortedBy {
- // Show normal tasks first and only then tasks with opened app selector
- it.topActivityComponent == appSelectorComponentName
+ /**
+ * Removes all recent tasks that are different from the profile of the host app to avoid any
+ * cross-profile sharing
+ */
+ private fun List<RecentTask>.filterDevicePolicyRestrictedTasks(): List<RecentTask> =
+ if (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+ // TODO(b/263950746): filter tasks based on the enterprise policies
+ this
+ } else {
+ filter { UserHandle.of(it.userId) == hostUserHandle }
}
+
+ private fun List<RecentTask>.sortedTasks(): List<RecentTask> = sortedBy {
+ // Show normal tasks first and only then tasks with opened app selector
+ it.topActivityComponent == appSelectorComponentName
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionBlockerEmptyStateProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionBlockerEmptyStateProvider.kt
new file mode 100644
index 0000000..829b0dd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionBlockerEmptyStateProvider.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.mediaprojection.appselector
+
+import android.content.Context
+import android.os.UserHandle
+import com.android.internal.R as AndroidR
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyState
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
+import com.android.internal.app.ResolverListAdapter
+import com.android.systemui.R
+import com.android.systemui.mediaprojection.devicepolicy.PersonalProfile
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
+import javax.inject.Inject
+
+@MediaProjectionAppSelectorScope
+class MediaProjectionBlockerEmptyStateProvider
+@Inject
+constructor(
+ @HostUserHandle private val hostAppHandle: UserHandle,
+ @PersonalProfile private val personalProfileHandle: UserHandle,
+ private val policyResolver: ScreenCaptureDevicePolicyResolver,
+ private val context: Context
+) : EmptyStateProvider {
+
+ override fun getEmptyState(resolverListAdapter: ResolverListAdapter): EmptyState? {
+ val screenCaptureAllowed =
+ policyResolver.isScreenCaptureAllowed(
+ targetAppUserHandle = resolverListAdapter.userHandle,
+ hostAppUserHandle = hostAppHandle
+ )
+
+ val isHostAppInPersonalProfile = hostAppHandle == personalProfileHandle
+
+ val subtitle =
+ if (isHostAppInPersonalProfile) {
+ AndroidR.string.resolver_cant_share_with_personal_apps_explanation
+ } else {
+ AndroidR.string.resolver_cant_share_with_work_apps_explanation
+ }
+
+ if (!screenCaptureAllowed) {
+ return object : EmptyState {
+ override fun getSubtitle(): String = context.resources.getString(subtitle)
+ override fun getTitle(): String =
+ context.resources.getString(
+ R.string.screen_capturing_disabled_by_policy_dialog_title
+ )
+ override fun onEmptyStateShown() {
+ // TODO(b/237397740) report analytics
+ }
+ override fun shouldSkipDataRebuild(): Boolean = true
+ }
+ }
+ return null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
index cd994b8..41e2286 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
@@ -17,11 +17,12 @@
package com.android.systemui.mediaprojection.appselector.data
import android.annotation.ColorInt
+import android.annotation.UserIdInt
import android.content.ComponentName
data class RecentTask(
val taskId: Int,
- val userId: Int,
+ @UserIdInt val userId: Int,
val topActivityComponent: ComponentName?,
val baseIntentComponent: ComponentName?,
@ColorInt val colorBackground: Int?
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskLabelLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskLabelLoader.kt
new file mode 100644
index 0000000..eadcb93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskLabelLoader.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.mediaprojection.appselector.data
+
+import android.annotation.UserIdInt
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+interface RecentTaskLabelLoader {
+ suspend fun loadLabel(userId: Int, componentName: ComponentName): CharSequence?
+}
+
+class ActivityTaskManagerLabelLoader
+@Inject
+constructor(
+ @Background private val coroutineDispatcher: CoroutineDispatcher,
+ private val packageManager: PackageManager
+) : RecentTaskLabelLoader {
+
+ override suspend fun loadLabel(
+ @UserIdInt userId: Int,
+ componentName: ComponentName
+ ): CharSequence? =
+ withContext(coroutineDispatcher) {
+ val userHandle = UserHandle(userId)
+ val appInfo =
+ packageManager.getApplicationInfo(
+ componentName.packageName,
+ PackageManager.ApplicationInfoFlags.of(0 /* no flags */)
+ )
+ val label = packageManager.getApplicationLabel(appInfo)
+ return@withContext packageManager.getUserBadgedLabel(label, userHandle)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
index 15cfeee..64f97f2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
@@ -20,11 +20,12 @@
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
-import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.android.systemui.R
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelector
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskLabelLoader
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import dagger.assisted.Assisted
@@ -40,9 +41,10 @@
@Assisted private val root: ViewGroup,
private val iconLoader: AppIconLoader,
private val thumbnailLoader: RecentTaskThumbnailLoader,
+ private val labelLoader: RecentTaskLabelLoader,
private val taskViewSizeProvider: TaskPreviewSizeProvider,
@MediaProjectionAppSelector private val scope: CoroutineScope
-) : RecyclerView.ViewHolder(root), ConfigurationListener, TaskPreviewSizeProvider.TaskPreviewSizeListener {
+) : ViewHolder(root), ConfigurationListener, TaskPreviewSizeProvider.TaskPreviewSizeListener {
val thumbnailView: MediaProjectionTaskView = root.requireViewById(R.id.task_thumbnail)
private val iconView: ImageView = root.requireViewById(R.id.task_icon)
@@ -64,6 +66,10 @@
val icon = iconLoader.loadIcon(task.userId, component)
iconView.setImageDrawable(icon)
}
+ launch {
+ val label = labelLoader.loadLabel(task.userId, component)
+ root.contentDescription = label
+ }
}
launch {
val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
@@ -88,10 +94,10 @@
private fun updateThumbnailSize() {
thumbnailView.layoutParams =
- thumbnailView.layoutParams.apply {
- width = taskViewSizeProvider.size.width()
- height = taskViewSizeProvider.size.height()
- }
+ thumbnailView.layoutParams.apply {
+ width = taskViewSizeProvider.size.width()
+ height = taskViewSizeProvider.size.height()
+ }
}
@AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/MediaProjectionDevicePolicyModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/MediaProjectionDevicePolicyModule.kt
new file mode 100644
index 0000000..13b71a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/MediaProjectionDevicePolicyModule.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.os.UserHandle
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import dagger.Module
+import dagger.Provides
+import javax.inject.Qualifier
+
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class WorkProfile
+
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class PersonalProfile
+
+/** Module for media projection device policy related dependencies */
+@Module
+class MediaProjectionDevicePolicyModule {
+ @Provides
+ @PersonalProfile
+ fun personalUserHandle(activityManagerWrapper: ActivityManagerWrapper): UserHandle {
+ // Current foreground user is the 'personal' profile
+ return UserHandle.of(activityManagerWrapper.currentUserId)
+ }
+
+ @Provides
+ @WorkProfile
+ fun workProfileUserHandle(userTracker: UserTracker): UserHandle? =
+ userTracker.userProfiles.find { it.isManagedProfile }?.userHandle
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt
new file mode 100644
index 0000000..6bd33e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.app.admin.DevicePolicyManager
+import android.os.UserHandle
+import android.os.UserManager
+import javax.inject.Inject
+
+/**
+ * Utility class to resolve if screen capture allowed for a particular target app/host app pair. It
+ * caches the state of the policies, so you need to create a new instance of this class if you want
+ * to react to updated policies state.
+ */
+class ScreenCaptureDevicePolicyResolver
+@Inject
+constructor(
+ private val devicePolicyManager: DevicePolicyManager,
+ private val userManager: UserManager,
+ @PersonalProfile private val personalProfileUserHandle: UserHandle,
+ @WorkProfile private val workProfileUserHandle: UserHandle?
+) {
+
+ /**
+ * Returns true if [hostAppUserHandle] is allowed to perform screen capture of
+ * [targetAppUserHandle]
+ */
+ fun isScreenCaptureAllowed(
+ targetAppUserHandle: UserHandle,
+ hostAppUserHandle: UserHandle,
+ ): Boolean {
+ if (hostAppUserHandle.isWorkProfile() && workProfileScreenCaptureDisabled) {
+ // Disable screen capturing as host apps should not capture the screen
+ return false
+ }
+
+ if (!hostAppUserHandle.isWorkProfile() && personalProfileScreenCaptureDisabled) {
+ // Disable screen capturing as personal apps should not capture the screen
+ return false
+ }
+
+ if (targetAppUserHandle.isWorkProfile()) {
+ // Work profile target
+ if (workProfileScreenCaptureDisabled) {
+ // Do not allow sharing work profile apps as work profile capturing is disabled
+ return false
+ }
+ } else {
+ // Personal profile target
+ if (hostAppUserHandle.isWorkProfile() && disallowSharingIntoManagedProfile) {
+ // Do not allow sharing of personal apps into work profile apps
+ return false
+ }
+
+ if (personalProfileScreenCaptureDisabled) {
+ // Disable screen capturing as personal apps should not be captured
+ return false
+ }
+ }
+
+ return true
+ }
+
+ /**
+ * Returns true if [hostAppUserHandle] is NOT allowed to capture an app from any profile,
+ * could be useful to finish the screen capture flow as soon as possible when the screen
+ * could not be captured at all.
+ */
+ fun isScreenCaptureCompletelyDisabled(hostAppUserHandle: UserHandle): Boolean {
+ val isWorkAppsCaptureDisabled =
+ if (workProfileUserHandle != null) {
+ !isScreenCaptureAllowed(
+ targetAppUserHandle = workProfileUserHandle,
+ hostAppUserHandle = hostAppUserHandle
+ )
+ } else true
+
+ val isPersonalAppsCaptureDisabled =
+ !isScreenCaptureAllowed(
+ targetAppUserHandle = personalProfileUserHandle,
+ hostAppUserHandle = hostAppUserHandle
+ )
+
+ return isWorkAppsCaptureDisabled && isPersonalAppsCaptureDisabled
+ }
+
+ private val personalProfileScreenCaptureDisabled: Boolean by lazy {
+ devicePolicyManager.getScreenCaptureDisabled(
+ /* admin */ null,
+ personalProfileUserHandle.identifier
+ )
+ }
+
+ private val workProfileScreenCaptureDisabled: Boolean by lazy {
+ workProfileUserHandle?.let {
+ devicePolicyManager.getScreenCaptureDisabled(/* admin */ null, it.identifier)
+ }
+ ?: false
+ }
+
+ private val disallowSharingIntoManagedProfile: Boolean by lazy {
+ workProfileUserHandle?.let {
+ userManager.hasUserRestrictionForUser(
+ UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE,
+ it
+ )
+ }
+ ?: false
+ }
+
+ private fun UserHandle?.isWorkProfile(): Boolean = this == workProfileUserHandle
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt
new file mode 100644
index 0000000..a6b3da0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Dialog that shows that screen capture is disabled on this device. */
+class ScreenCaptureDisabledDialog(context: Context) : SystemUIDialog(context) {
+
+ init {
+ setTitle(context.getString(R.string.screen_capturing_disabled_by_policy_dialog_title))
+ setMessage(
+ context.getString(R.string.screen_capturing_disabled_by_policy_dialog_description)
+ )
+ setIcon(R.drawable.ic_cast)
+ setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> cancel() }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
index 3ecf154..8d80990 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java
@@ -16,13 +16,12 @@
package com.android.systemui.model;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import android.annotation.NonNull;
import android.util.Log;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.system.QuickStepContract;
import java.io.PrintWriter;
@@ -39,11 +38,16 @@
private static final String TAG = SysUiState.class.getSimpleName();
public static final boolean DEBUG = false;
+ private final DisplayTracker mDisplayTracker;
private @QuickStepContract.SystemUiStateFlags int mFlags;
private final List<SysUiStateCallback> mCallbacks = new ArrayList<>();
private int mFlagsToSet = 0;
private int mFlagsToClear = 0;
+ public SysUiState(DisplayTracker displayTracker) {
+ mDisplayTracker = displayTracker;
+ }
+
/**
* Add listener to be notified of changes made to SysUI state.
* The callback will also be called as part of this function.
@@ -81,7 +85,7 @@
}
private void updateFlags(int displayId) {
- if (displayId != DEFAULT_DISPLAY) {
+ if (displayId != mDisplayTracker.getDefaultDisplayId()) {
// Ignore non-default displays for now
Log.w(TAG, "Ignoring flag update for display: " + displayId, new Throwable());
return;
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
index 1324d2c..c4a1ed4 100644
--- a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
@@ -19,7 +19,6 @@
import android.view.WindowManagerGlobal
import com.android.app.motiontool.DdmHandleMotionTool
import com.android.app.motiontool.MotionToolManager
-import com.android.app.viewcapture.ViewCapture
import com.android.systemui.CoreStartable
import dagger.Binds
import dagger.Module
@@ -38,17 +37,12 @@
}
@Provides
- fun provideMotionToolManager(
- viewCapture: ViewCapture,
- windowManagerGlobal: WindowManagerGlobal
- ): MotionToolManager {
- return MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+ fun provideMotionToolManager(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+ return MotionToolManager.getInstance(windowManagerGlobal)
}
@Provides
fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance()
-
- @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance()
}
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index a92203c..1121e160 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -297,7 +297,7 @@
private void updateAssistantAvailability() {
boolean assistantAvailableForUser = mAssistManagerLazy.get()
- .getAssistInfoForUser(UserHandle.USER_CURRENT) != null;
+ .getAssistInfoForUser(mUserTracker.getUserId()) != null;
boolean longPressDefault = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault);
mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 26c1083..888918e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -16,6 +16,7 @@
package com.android.systemui.navigationbar;
+import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
@@ -24,7 +25,6 @@
import static android.app.StatusBarManager.WindowVisibleState;
import static android.app.StatusBarManager.windowStateToString;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.InsetsState.containsType;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
@@ -44,6 +44,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
@@ -70,7 +71,6 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.telecom.TelecomManager;
import android.text.TextUtils;
@@ -129,6 +129,7 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
@@ -136,9 +137,10 @@
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.rotation.RotationButton;
import com.android.systemui.shared.rotation.RotationButtonController;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.AutoHideUiElement;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
@@ -216,6 +218,7 @@
private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
private final UserContextProvider mUserContextProvider;
private final WakefulnessLifecycle mWakefulnessLifecycle;
+ private final DisplayTracker mDisplayTracker;
private final RegionSamplingHelper mRegionSamplingHelper;
private final int mNavColorSampleMargin;
private NavigationBarFrame mFrame;
@@ -252,6 +255,7 @@
private final AutoHideController.Factory mAutoHideControllerFactory;
private final Optional<TelecomManager> mTelecomManagerOptional;
private final InputMethodManager mInputMethodManager;
+ private final TaskStackChangeListeners mTaskStackChangeListeners;
@VisibleForTesting
public int mDisplayId;
@@ -458,7 +462,8 @@
@Override
public void onStartedWakingUp() {
notifyScreenStateChanged(true);
- if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) {
+ if (isGesturalModeOnDefaultDisplay(getContext(), mDisplayTracker,
+ mNavBarMode)) {
mRegionSamplingHelper.start(mSamplingBounds);
}
}
@@ -488,6 +493,18 @@
}
};
+ private boolean mScreenPinningActive = false;
+ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+ @Override
+ public void onLockTaskModeChanged(int mode) {
+ mScreenPinningActive = (mode == LOCK_TASK_MODE_PINNED);
+ mSysUiFlagsContainer.setFlag(SYSUI_STATE_SCREEN_PINNING, mScreenPinningActive)
+ .commitUpdate(mDisplayId);
+ mView.setInScreenPinning(mScreenPinningActive);
+ updateScreenPinningGestures();
+ }
+ };
+
@Inject
NavigationBar(
NavigationBarView navigationBarView,
@@ -529,7 +546,9 @@
EdgeBackGestureHandler edgeBackGestureHandler,
Optional<BackAnimation> backAnimation,
UserContextProvider userContextProvider,
- WakefulnessLifecycle wakefulnessLifecycle) {
+ WakefulnessLifecycle wakefulnessLifecycle,
+ TaskStackChangeListeners taskStackChangeListeners,
+ DisplayTracker displayTracker) {
super(navigationBarView);
mFrame = navigationBarFrame;
mContext = context;
@@ -568,6 +587,8 @@
mInputMethodManager = inputMethodManager;
mUserContextProvider = userContextProvider;
mWakefulnessLifecycle = wakefulnessLifecycle;
+ mTaskStackChangeListeners = taskStackChangeListeners;
+ mDisplayTracker = displayTracker;
mNavColorSampleMargin = getResources()
.getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin);
@@ -615,12 +636,14 @@
@Override
public boolean isSamplingEnabled() {
- return isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode);
+ return isGesturalModeOnDefaultDisplay(getContext(), mDisplayTracker,
+ mNavBarMode);
}
}, mainExecutor, bgExecutor);
mView.setBackgroundExecutor(bgExecutor);
mView.setEdgeBackGestureHandler(mEdgeBackGestureHandler);
+ mView.setDisplayTracker(mDisplayTracker);
mNavBarMode = mNavigationModeController.addListener(mModeChangedListener);
}
@@ -648,7 +671,7 @@
getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
.getRotation()));
mDisplayId = mContext.getDisplayId();
- mIsOnDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
+ mIsOnDefaultDisplay = mDisplayId == mDisplayTracker.getDefaultDisplayId();
// Ensure we try to get currentSysuiState from navBarHelper before command queue callbacks
// start firing, since the latter is source of truth
@@ -676,6 +699,7 @@
mCommandQueue.recomputeDisableFlags(mDisplayId, false);
mNotificationShadeDepthController.addListener(mDepthListener);
+ mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
}
public void destroyView() {
@@ -689,6 +713,7 @@
mNotificationShadeDepthController.removeListener(mDepthListener);
mDeviceConfigProxy.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
+ mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener);
}
@Override
@@ -990,11 +1015,15 @@
pw.println(" mTransientShown=" + mTransientShown);
pw.println(" mTransientShownFromGestureOnSystemBar="
+ mTransientShownFromGestureOnSystemBar);
+ pw.println(" mScreenPinningActive=" + mScreenPinningActive);
dumpBarTransitions(pw, "mNavigationBarView", getBarTransitions());
pw.println(" mOrientedHandleSamplingRegion: " + mOrientedHandleSamplingRegion);
mView.dump(pw);
mRegionSamplingHelper.dump(pw);
+ if (mAutoHideController != null) {
+ mAutoHideController.dump(pw);
+ }
}
// ----- CommandQueue Callbacks -----
@@ -1213,10 +1242,9 @@
private void updateScreenPinningGestures() {
// Change the cancel pin gesture to home and back if recents button is invisible
- boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
ButtonDispatcher backButton = mView.getBackButton();
ButtonDispatcher recentsButton = mView.getRecentsButton();
- if (pinningActive) {
+ if (mScreenPinningActive) {
boolean recentsVisible = mView.isRecentsButtonVisible();
backButton.setOnLongClickListener(recentsVisible
? this::onLongPressBackRecents
@@ -1227,8 +1255,8 @@
recentsButton.setOnLongClickListener(null);
}
// Note, this needs to be set after even if we're setting the listener to null
- backButton.setLongClickable(pinningActive);
- recentsButton.setLongClickable(pinningActive);
+ backButton.setLongClickable(mScreenPinningActive);
+ recentsButton.setLongClickable(mScreenPinningActive);
}
private void notifyNavigationBarScreenOn() {
@@ -1311,8 +1339,7 @@
@VisibleForTesting
boolean onHomeLongClick(View v) {
- if (!mView.isRecentsButtonVisible()
- && ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
+ if (!mView.isRecentsButtonVisible() && mScreenPinningActive) {
return onLongPressBackHome(v);
}
if (shouldDisableNavbarGestures()) {
@@ -1447,7 +1474,7 @@
private void onAccessibilityClick(View v) {
final Display display = v.getDisplay();
mAccessibilityManager.notifyAccessibilityButtonClicked(
- display != null ? display.getDisplayId() : DEFAULT_DISPLAY);
+ display != null ? display.getDisplayId() : mDisplayTracker.getDefaultDisplayId());
}
private boolean onAccessibilityLongClick(View v) {
@@ -1455,7 +1482,7 @@
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final String chooserClassName = AccessibilityButtonChooserActivity.class.getName();
intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
- mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
return true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 8914552..8c19111 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -19,12 +19,10 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
-import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
import static com.android.systemui.shared.recents.utilities.Utilities.isTablet;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
@@ -56,14 +54,16 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.model.SysUiState;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.phone.AutoHideController;
import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.settings.SecureSettings;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.pip.Pip;
@@ -86,9 +86,10 @@
private final Handler mHandler;
private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
private FeatureFlags mFeatureFlags;
+ private final SecureSettings mSecureSettings;
+ private final DisplayTracker mDisplayTracker;
private final DisplayManager mDisplayManager;
private final TaskbarDelegate mTaskbarDelegate;
- private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private int mNavMode;
@VisibleForTesting boolean mIsTablet;
@@ -112,28 +113,31 @@
NavBarHelper navBarHelper,
TaskbarDelegate taskbarDelegate,
NavigationBarComponent.Factory navigationBarComponentFactory,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager,
DumpManager dumpManager,
AutoHideController autoHideController,
LightBarController lightBarController,
+ TaskStackChangeListeners taskStackChangeListeners,
Optional<Pip> pipOptional,
Optional<BackAnimation> backAnimation,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ SecureSettings secureSettings,
+ DisplayTracker displayTracker) {
mContext = context;
mHandler = mainHandler;
mNavigationBarComponentFactory = navigationBarComponentFactory;
mFeatureFlags = featureFlags;
+ mSecureSettings = secureSettings;
+ mDisplayTracker = displayTracker;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
commandQueue.addCallback(this);
configurationController.addCallback(this);
mConfigChanges.applyNewConfig(mContext.getResources());
mNavMode = navigationModeController.addListener(this);
mTaskbarDelegate = taskbarDelegate;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService,
navBarHelper, navigationModeController, sysUiFlagsContainer,
dumpManager, autoHideController, lightBarController, pipOptional,
- backAnimation.orElse(null));
+ backAnimation.orElse(null), taskStackChangeListeners);
mIsTablet = isTablet(mContext);
dumpManager.registerDumpable(this);
}
@@ -193,8 +197,7 @@
}
private void updateAccessibilityButtonModeIfNeeded() {
- ContentResolver contentResolver = mContext.getContentResolver();
- final int mode = Settings.Secure.getIntForUser(contentResolver,
+ final int mode = mSecureSettings.getIntForUser(
Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
@@ -208,14 +211,14 @@
// force update to ACCESSIBILITY_BUTTON_MODE_GESTURE.
if (QuickStepContract.isGesturalMode(mNavMode)
&& mode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR) {
- Settings.Secure.putIntForUser(contentResolver,
+ mSecureSettings.putIntForUser(
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_GESTURE,
UserHandle.USER_CURRENT);
// ACCESSIBILITY_BUTTON_MODE_GESTURE is incompatible under non gestural mode. Need to
// force update to ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR.
} else if (!QuickStepContract.isGesturalMode(mNavMode)
&& mode == ACCESSIBILITY_BUTTON_MODE_GESTURE) {
- Settings.Secure.putIntForUser(contentResolver,
+ mSecureSettings.putIntForUser(
Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
}
@@ -296,9 +299,10 @@
// Don't need to create nav bar on the default display if we initialize TaskBar.
final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
&& !initializeTaskbarIfNecessary();
- Display[] displays = mDisplayManager.getDisplays();
+ Display[] displays = mDisplayTracker.getAllDisplays();
for (Display display : displays) {
- if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) {
+ if (shouldCreateDefaultNavbar
+ || display.getDisplayId() != mDisplayTracker.getDefaultDisplayId()) {
createNavigationBar(display, null /* savedState */, result);
}
}
@@ -317,7 +321,7 @@
}
final int displayId = display.getDisplayId();
- final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;
+ final boolean isOnDefaultDisplay = displayId == mDisplayTracker.getDefaultDisplayId();
// We may show TaskBar on the default display for large screen device. Don't need to create
// navigation bar for this case.
@@ -412,7 +416,7 @@
/** @return {@link NavigationBarView} on the default display. */
public @Nullable NavigationBarView getDefaultNavigationBarView() {
- return getNavigationBarView(DEFAULT_DISPLAY);
+ return getNavigationBarView(mDisplayTracker.getDefaultDisplayId());
}
/**
@@ -433,7 +437,8 @@
final NavigationBarView navBarView = getNavigationBarView(displayId);
if (navBarView != null) {
navBarView.showPinningEnterExitToast(entering);
- } else if (displayId == DEFAULT_DISPLAY && mTaskbarDelegate.isInitialized()) {
+ } else if (displayId == mDisplayTracker.getDefaultDisplayId()
+ && mTaskbarDelegate.isInitialized()) {
mTaskbarDelegate.showPinningEnterExitToast(entering);
}
}
@@ -442,7 +447,8 @@
final NavigationBarView navBarView = getNavigationBarView(displayId);
if (navBarView != null) {
navBarView.showPinningEscapeToast();
- } else if (displayId == DEFAULT_DISPLAY && mTaskbarDelegate.isInitialized()) {
+ } else if (displayId == mDisplayTracker.getDefaultDisplayId()
+ && mTaskbarDelegate.isInitialized()) {
mTaskbarDelegate.showPinningEscapeToast();
}
}
@@ -459,7 +465,7 @@
/** @return {@link NavigationBar} on the default display. */
@Nullable
public NavigationBar getDefaultNavigationBar() {
- return mNavigationBars.get(DEFAULT_DISPLAY);
+ return mNavigationBars.get(mDisplayTracker.getDefaultDisplayId());
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
index 6793f01..a4de9ff 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarTransitions.java
@@ -24,7 +24,6 @@
import android.os.Handler;
import android.os.RemoteException;
import android.util.SparseArray;
-import android.view.Display;
import android.view.IWallpaperVisibilityListener;
import android.view.IWindowManager;
import android.view.View;
@@ -32,6 +31,7 @@
import com.android.systemui.R;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.phone.BarTransitions;
import com.android.systemui.statusbar.phone.LightBarTransitionsController;
@@ -65,6 +65,7 @@
@org.jetbrains.annotations.NotNull
private final IWindowManager mWindowManagerService;
private final LightBarTransitionsController mLightTransitionsController;
+ private final DisplayTracker mDisplayTracker;
private final boolean mAllowAutoDimWallpaperNotVisible;
private boolean mWallpaperVisible;
@@ -89,18 +90,20 @@
public NavigationBarTransitions(
NavigationBarView view,
IWindowManager windowManagerService,
- LightBarTransitionsController.Factory lightBarTransitionsControllerFactory) {
+ LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
+ DisplayTracker displayTracker) {
super(view, R.drawable.nav_background);
mView = view;
mWindowManagerService = windowManagerService;
mLightTransitionsController = lightBarTransitionsControllerFactory.create(this);
+ mDisplayTracker = displayTracker;
mAllowAutoDimWallpaperNotVisible = view.getContext().getResources()
.getBoolean(R.bool.config_navigation_bar_enable_auto_dim_no_visible_wallpaper);
mDarkIntensityListeners = new ArrayList();
try {
mWallpaperVisible = mWindowManagerService.registerWallpaperVisibilityListener(
- mWallpaperVisibilityListener, Display.DEFAULT_DISPLAY);
+ mWallpaperVisibilityListener, mDisplayTracker.getDefaultDisplayId());
} catch (RemoteException e) {
}
mView.addOnLayoutChangeListener(
@@ -126,7 +129,7 @@
public void destroy() {
try {
mWindowManagerService.unregisterWallpaperVisibilityListener(mWallpaperVisibilityListener,
- Display.DEFAULT_DISPLAY);
+ mDisplayTracker.getDefaultDisplayId());
} catch (RemoteException e) {
}
mLightTransitionsController.destroy();
@@ -135,7 +138,10 @@
@Override
public void setAutoDim(boolean autoDim) {
// Ensure we aren't in gestural nav if we are triggering auto dim
- if (autoDim && isGesturalModeOnDefaultDisplay(mView.getContext(), mNavBarMode)) return;
+ if (autoDim && isGesturalModeOnDefaultDisplay(mView.getContext(), mDisplayTracker,
+ mNavBarMode)) {
+ return;
+ }
if (mAutoDim == autoDim) return;
mAutoDim = autoDim;
applyLightsOut(true, false);
@@ -219,7 +225,7 @@
@Override
public int getTintAnimationDuration() {
- if (isGesturalModeOnDefaultDisplay(mView.getContext(), mNavBarMode)) {
+ if (isGesturalModeOnDefaultDisplay(mView.getContext(), mDisplayTracker, mNavBarMode)) {
return Math.max(DEFAULT_COLOR_ADAPT_TRANSITION_TIME, MIN_COLOR_ADAPT_TRANSITION_TIME);
}
return LightBarTransitionsController.DEFAULT_TINT_ANIMATION_DURATION;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
index 403d276..63fb499 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
@@ -21,7 +21,6 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SEARCH_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
@@ -74,11 +73,11 @@
import com.android.systemui.navigationbar.buttons.RotationContextButton;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shared.rotation.FloatingRotationButton;
import com.android.systemui.shared.rotation.RotationButton.RotationButtonUpdatesCallback;
import com.android.systemui.shared.rotation.RotationButtonController;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.phone.AutoHideController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -125,6 +124,7 @@
private int mDarkIconColor;
private EdgeBackGestureHandler mEdgeBackGestureHandler;
+ private DisplayTracker mDisplayTracker;
private final DeadZone mDeadZone;
private NavigationBarTransitions mBarTransitions;
@Nullable
@@ -160,6 +160,7 @@
* fully locked mode we only show that unlocking is blocked.
*/
private ScreenPinningNotify mScreenPinningNotify;
+ private boolean mScreenPinningActive = false;
/**
* {@code true} if the IME can render the back button and the IME switcher button.
@@ -300,7 +301,8 @@
R.dimen.floating_rotation_button_taskbar_left_margin,
R.dimen.floating_rotation_button_taskbar_bottom_margin,
R.dimen.floating_rotation_button_diameter,
- R.dimen.key_button_ripple_max_width);
+ R.dimen.key_button_ripple_max_width,
+ R.bool.floating_rotation_button_position_left);
mRotationButtonController = new RotationButtonController(mLightContext, mLightIconColor,
mDarkIconColor, R.drawable.ic_sysbar_rotate_button_ccw_start_0,
R.drawable.ic_sysbar_rotate_button_ccw_start_90,
@@ -358,6 +360,10 @@
mBgExecutor = bgExecutor;
}
+ public void setDisplayTracker(DisplayTracker displayTracker) {
+ mDisplayTracker = displayTracker;
+ }
+
public void setTouchHandler(Gefingerpoken touchHandler) {
mTouchHandler = touchHandler;
}
@@ -555,7 +561,8 @@
}
public void setBehavior(@Behavior int behavior) {
- mRotationButtonController.onBehaviorChanged(Display.DEFAULT_DISPLAY, behavior);
+ mRotationButtonController.onBehaviorChanged(mDisplayTracker.getDefaultDisplayId(),
+ behavior);
}
@Override
@@ -636,14 +643,13 @@
// When screen pinning, don't hide back and home when connected service or back and
// recents buttons when disconnected from launcher service in screen pinning mode,
// as they are used for exiting.
- final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
if (mOverviewProxyEnabled) {
// Force disable recents when not in legacy mode
disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode);
- if (pinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) {
+ if (mScreenPinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) {
disableBack = disableHome = false;
}
- } else if (pinningActive) {
+ } else if (mScreenPinningActive) {
disableBack = disableRecent = false;
}
@@ -676,7 +682,7 @@
@VisibleForTesting
boolean isRecentsButtonDisabled() {
return mUseCarModeUi || !isOverviewEnabled()
- || getContext().getDisplayId() != Display.DEFAULT_DISPLAY;
+ || getContext().getDisplayId() != mDisplayTracker.getDefaultDisplayId();
}
private Display getContextDisplay() {
@@ -738,9 +744,7 @@
public void updateDisabledSystemUiStateFlags(SysUiState sysUiState) {
int displayId = mContext.getDisplayId();
- sysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING,
- ActivityManagerWrapper.getInstance().isScreenPinningActive())
- .setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
+ sysUiState.setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
(mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0)
.setFlag(SYSUI_STATE_HOME_DISABLED,
(mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0)
@@ -749,6 +753,10 @@
.commitUpdate(displayId);
}
+ public void setInScreenPinning(boolean active) {
+ mScreenPinningActive = active;
+ }
+
private void updatePanelSystemUiStateFlags() {
if (SysUiState.DEBUG) {
Log.d(TAG, "Updating panel sysui state flags: panelView=" + mPanelView);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 5e26e60..6ee86aa 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -16,6 +16,7 @@
package com.android.systemui.navigationbar;
+import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
@@ -40,6 +41,7 @@
import android.app.StatusBarManager;
import android.app.StatusBarManager.WindowVisibleState;
+import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -68,6 +70,8 @@
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.AutoHideUiElement;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.AutoHideController;
@@ -101,6 +105,7 @@
private AutoHideController mAutoHideController;
private LightBarController mLightBarController;
private LightBarTransitionsController mLightBarTransitionsController;
+ private TaskStackChangeListeners mTaskStackChangeListeners;
private Optional<Pip> mPipOptional;
private int mDisplayId;
private int mNavigationIconHints;
@@ -127,6 +132,14 @@
private final DisplayManager mDisplayManager;
private Context mWindowContext;
private ScreenPinningNotify mScreenPinningNotify;
+ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+ @Override
+ public void onLockTaskModeChanged(int mode) {
+ mSysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, mode == LOCK_TASK_MODE_PINNED)
+ .commitUpdate(mDisplayId);
+ }
+ };
+
private int mNavigationMode = -1;
private final Consumer<Rect> mPipListener;
@@ -176,7 +189,8 @@
AutoHideController autoHideController,
LightBarController lightBarController,
Optional<Pip> pipOptional,
- BackAnimation backAnimation) {
+ BackAnimation backAnimation,
+ TaskStackChangeListeners taskStackChangeListeners) {
// TODO: adding this in the ctor results in a dagger dependency cycle :(
mCommandQueue = commandQueue;
mOverviewProxyService = overviewProxyService;
@@ -189,6 +203,7 @@
mPipOptional = pipOptional;
mBackAnimation = backAnimation;
mLightBarTransitionsController = createLightBarTransitionsController();
+ mTaskStackChangeListeners = taskStackChangeListeners;
}
// Separated into a method to keep setDependencies() clean/readable.
@@ -234,6 +249,7 @@
mPipOptional.ifPresent(this::addPipExclusionBoundsChangeListener);
mEdgeBackGestureHandler.setBackAnimation(mBackAnimation);
mEdgeBackGestureHandler.onConfigurationChanged(mContext.getResources().getConfiguration());
+ mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
mInitialized = true;
}
@@ -253,6 +269,7 @@
mLightBarTransitionsController.destroy();
mLightBarController.setNavigationBar(null);
mPipOptional.ifPresent(this::removePipExclusionBoundsChangeListener);
+ mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener);
mInitialized = false;
}
@@ -300,8 +317,6 @@
.setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible())
.setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
allowSystemGestureIgnoringBarVisibility())
- .setFlag(SYSUI_STATE_SCREEN_PINNING,
- ActivityManagerWrapper.getInstance().isScreenPinningActive())
.setFlag(SYSUI_STATE_IMMERSIVE_MODE, isImmersiveMode())
.commitUpdate(mDisplayId);
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 13c5b48..eea0e4c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -273,7 +273,6 @@
@Override
public void triggerBack() {
// Notify FalsingManager that an intentional gesture has occurred.
- // TODO(b/186519446): use a different method than isFalseTouch
mFalsingManager.isFalseTouch(BACK_GESTURE);
// Only inject back keycodes when ahead-of-time back dispatching is disabled.
if (mBackAnimation == null) {
@@ -502,6 +501,15 @@
}
private void updateIsEnabled() {
+ try {
+ Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
+ updateIsEnabledTraced();
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ private void updateIsEnabledTraced() {
boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
if (isEnabled == mIsEnabled) {
return;
@@ -587,13 +595,18 @@
}
private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) {
- if (mEdgeBackPlugin != null) {
- mEdgeBackPlugin.onDestroy();
+ try {
+ Trace.beginSection("setEdgeBackPlugin");
+ if (mEdgeBackPlugin != null) {
+ mEdgeBackPlugin.onDestroy();
+ }
+ mEdgeBackPlugin = edgeBackPlugin;
+ mEdgeBackPlugin.setBackCallback(mBackCallback);
+ mEdgeBackPlugin.setLayoutParams(createLayoutParams());
+ updateDisplaySize();
+ } finally {
+ Trace.endSection();
}
- mEdgeBackPlugin = edgeBackPlugin;
- mEdgeBackPlugin.setBackCallback(mBackCallback);
- mEdgeBackPlugin.setLayoutParams(createLayoutParams());
- updateDisplaySize();
}
public boolean isHandlingGestures() {
@@ -919,6 +932,10 @@
mThresholdCrossed = true;
// Capture inputs
mInputMonitor.pilferPointers();
+ if (mBackAnimation != null) {
+ // Notify FalsingManager that an intentional gesture has occurred.
+ mFalsingManager.isFalseTouch(BACK_GESTURE);
+ }
mInputEventReceiver.setBatchingEnabled(true);
} else {
logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_FAR_FROM_EDGE);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 1230708..590efbb 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -16,8 +16,6 @@
package com.android.systemui.navigationbar.gestural;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE;
import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
@@ -56,6 +54,7 @@
import com.android.systemui.animation.Interpolators;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.NavigationEdgeBackPlugin;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.statusbar.VibratorHelper;
@@ -289,7 +288,8 @@
Context context,
LatencyTracker latencyTracker,
VibratorHelper vibratorHelper,
- @Background Executor backgroundExecutor) {
+ @Background Executor backgroundExecutor,
+ DisplayTracker displayTracker) {
super(context);
mWindowManager = context.getSystemService(WindowManager.class);
@@ -365,7 +365,7 @@
setVisibility(GONE);
- boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY;
+ boolean isPrimaryDisplay = mContext.getDisplayId() == displayTracker.getDefaultDisplayId();
mRegionSamplingHelper = new RegionSamplingHelper(this,
new RegionSamplingHelper.SamplingCallback() {
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 6dd60d0..6bfe1a0 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -17,10 +17,12 @@
package com.android.systemui.notetask
import android.app.KeyguardManager
+import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.os.UserManager
+import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull
@@ -57,7 +59,9 @@
* If the keyguard is locked, notes will open as a full screen experience. A locked device has
* no contextual information which let us use the whole screen space available.
*
- * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience.
+ * If not in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be
+ * collapsed if the notes bubble is already opened.
+ *
* That will let users open other apps in full screen, and take contextual notes.
*/
fun showNoteTask(isInMultiWindowMode: Boolean = false) {
@@ -66,16 +70,23 @@
val bubbles = optionalBubbles.getOrNull() ?: return
val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
val userManager = optionalUserManager.getOrNull() ?: return
- val intent = intentResolver.resolveIntent() ?: return
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
- if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
- context.startActivity(intent)
- } else {
- // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
- bubbles.showAppBubble(intent)
+ val intent = intentResolver.resolveIntent() ?: return
+
+ // TODO(b/266686199): We should handle when app not available. For now, we log.
+ try {
+ if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
+ context.startActivity(intent)
+ } else {
+ bubbles.showOrHideAppBubble(intent)
+ }
+ } catch (e: ActivityNotFoundException) {
+ val message =
+ "Activity not found for action: ${NoteTaskIntentResolver.ACTION_CREATE_NOTE}."
+ Log.e(TAG, message, e)
}
}
@@ -102,4 +113,11 @@
PackageManager.DONT_KILL_APP,
)
}
+
+ companion object {
+ private val TAG = NoteTaskController::class.simpleName.orEmpty()
+
+ // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
+ const val NOTE_TASK_KEY_EVENT = 311
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d14b7a7..d5f4a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -16,7 +16,6 @@
package com.android.systemui.notetask
-import android.view.KeyEvent
import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.CommandQueue
import com.android.wm.shell.bubbles.Bubbles
@@ -37,7 +36,7 @@
val callbacks =
object : CommandQueue.Callbacks {
override fun handleSystemKey(keyCode: Int) {
- if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+ if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) {
noteTaskController.showNoteTask()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
index 98d6991..11dc1d7 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
@@ -16,66 +16,39 @@
package com.android.systemui.notetask
-import android.content.ComponentName
+import android.app.role.RoleManager
+import android.content.Context
import android.content.Intent
-import android.content.pm.ActivityInfo
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.ResolveInfoFlags
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
import javax.inject.Inject
-/**
- * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
- * [Intent] ready for be launched will be returned. Otherwise, returns null.
- *
- * TODO(b/248274123): should be revisited once the notes role is implemented.
- */
internal class NoteTaskIntentResolver
@Inject
constructor(
- private val packageManager: PackageManager,
+ private val context: Context,
+ private val roleManager: RoleManager,
) {
fun resolveIntent(): Intent? {
- val intent = Intent(NOTES_ACTION)
- val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
- val infoList = packageManager.queryIntentActivities(intent, flags)
+ val packageName = roleManager.getRoleHoldersAsUser(ROLE_NOTES, context.user).firstOrNull()
- for (info in infoList) {
- val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
- val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
+ if (packageName.isNullOrEmpty()) return null
- return Intent(NOTES_ACTION)
- .setComponent(ComponentName(packageName, activityName))
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
-
- return null
- }
-
- private fun resolveActivityNameForNotesAction(packageName: String): String? {
- val intent = Intent(NOTES_ACTION).setPackage(packageName)
- val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
- val resolveInfo = packageManager.resolveActivity(intent, flags)
-
- val activityInfo = resolveInfo?.activityInfo ?: return null
- if (activityInfo.name.isNullOrBlank()) return null
- if (!activityInfo.exported) return null
- if (!activityInfo.enabled) return null
- if (!activityInfo.showWhenLocked) return null
- if (!activityInfo.turnScreenOn) return null
-
- return activityInfo.name
+ return Intent(ACTION_CREATE_NOTE)
+ .setPackage(packageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint was
+ // used to start it.
+ .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
}
companion object {
- // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
- const val NOTES_ACTION = "android.intent.action.NOTES"
+ // TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
+ const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
+
+ // TODO(b/265912743): Use RoleManager.NOTES_ROLE instead.
+ const val ROLE_NOTES = "android.app.role.NOTES"
+
+ // TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
+ const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
}
}
-
-private val ActivityInfo.showWhenLocked: Boolean
- get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0
-
-private val ActivityInfo.turnScreenOn: Boolean
- get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 8bdf319..ec6a16a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -18,11 +18,13 @@
import android.app.Activity
import android.app.KeyguardManager
+import android.app.role.RoleManager
import android.content.Context
import android.os.UserManager
import androidx.core.content.getSystemService
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
import dagger.Binds
@@ -33,20 +35,25 @@
import java.util.Optional
/** Compose all dependencies required by Note Task feature. */
-@Module
+@Module(includes = [NoteTaskQuickAffordanceModule::class])
internal interface NoteTaskModule {
@[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
- fun bindNoteTaskLauncherActivity(activity: LaunchNoteTaskActivity): Activity?
+ fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
@[Binds IntoMap ClassKey(CreateNoteTaskShortcutActivity::class)]
- fun bindNoteTaskShortcutActivity(activity: CreateNoteTaskShortcutActivity): Activity?
+ fun CreateNoteTaskShortcutActivity.bindNoteTaskShortcutActivity(): Activity
companion object {
@[Provides NoteTaskEnabledKey]
- fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
- return featureFlags.isEnabled(Flags.NOTE_TASKS)
+ fun provideIsNoteTaskEnabled(
+ featureFlags: FeatureFlags,
+ roleManager: RoleManager,
+ ): Boolean {
+ val isRoleAvailable = roleManager.isRoleAvailable(NoteTaskIntentResolver.ROLE_NOTES)
+ val isFeatureEnabled = featureFlags.isEnabled(Flags.NOTE_TASKS)
+ return isRoleAvailable && isFeatureEnabled
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
new file mode 100644
index 0000000..cfbaa48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.notetask.quickaffordance
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.PickerScreenState
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.notetask.NoteTaskEnabledKey
+import javax.inject.Inject
+import kotlinx.coroutines.flow.flowOf
+
+internal class NoteTaskQuickAffordanceConfig
+@Inject
+constructor(
+ context: Context,
+ private val noteTaskController: NoteTaskController,
+ @NoteTaskEnabledKey private val isEnabled: Boolean,
+) : KeyguardQuickAffordanceConfig {
+
+ override val key = BuiltInKeyguardQuickAffordanceKeys.CREATE_NOTE
+
+ override val pickerName: String = context.getString(R.string.note_task_button_label)
+
+ override val pickerIconResourceId = R.drawable.ic_note_task_shortcut_keyguard
+
+ override val lockScreenState = flowOf(getLockScreenState())
+
+ // TODO(b/265949213)
+ private fun getLockScreenState() =
+ if (isEnabled) {
+ val icon = Icon.Resource(pickerIconResourceId, ContentDescription.Loaded(pickerName))
+ LockScreenState.Visible(icon)
+ } else {
+ LockScreenState.Hidden
+ }
+
+ override suspend fun getPickerScreenState() =
+ if (isEnabled) {
+ PickerScreenState.Default()
+ } else {
+ PickerScreenState.UnavailableOnDevice
+ }
+
+ override fun onTriggered(expandable: Expandable?): OnTriggeredResult {
+ noteTaskController.showNoteTask()
+ return OnTriggeredResult.Handled
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
new file mode 100644
index 0000000..7cb932a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask.quickaffordance
+
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+internal interface NoteTaskQuickAffordanceModule {
+
+ @[Binds IntoSet]
+ fun NoteTaskQuickAffordanceConfig.bindNoteTaskQuickAffordance(): KeyguardQuickAffordanceConfig
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
index f6a623e..6ab0da6 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/CreateNoteTaskShortcutActivity.kt
@@ -46,7 +46,7 @@
id = SHORTCUT_ID,
shortLabel = getString(R.string.note_task_button_label),
intent = LaunchNoteTaskActivity.newIntent(context = this),
- iconResource = R.drawable.ic_note_task_button,
+ iconResource = R.drawable.ic_note_task_shortcut_widget,
)
setResult(Activity.RESULT_OK, intent)
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index 47fe676..f203e7a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -45,8 +45,8 @@
fun newIntent(context: Context): Intent {
return Intent(context, LaunchNoteTaskActivity::class.java).apply {
// Intent's action must be set in shortcuts, or an exception will be thrown.
- // TODO(b/254606432): Use Intent.ACTION_NOTES instead.
- action = NoteTaskIntentResolver.NOTES_ACTION
+ // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead.
+ action = NoteTaskIntentResolver.ACTION_CREATE_NOTE
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index fba5f63..7f0f894 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -68,8 +68,10 @@
};
if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ Log.d(TAG, "Using the Compose implementation of the PeopleSpaceActivity");
ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
} else {
+ Log.d(TAG, "Using the View implementation of the PeopleSpaceActivity");
ViewGroup view = PeopleViewBinder.create(this);
PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
setContentView(view);
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index 90fc1d7..595b882 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -68,6 +68,7 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.NotificationChannels;
@@ -175,7 +176,7 @@
private ActivityStarter mActivityStarter;
private final BroadcastSender mBroadcastSender;
private final UiEventLogger mUiEventLogger;
-
+ private final UserTracker mUserTracker;
private final Lazy<BatteryController> mBatteryControllerLazy;
private final DialogLaunchAnimator mDialogLaunchAnimator;
@@ -184,7 +185,8 @@
@Inject
public PowerNotificationWarnings(Context context, ActivityStarter activityStarter,
BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy,
- DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger) {
+ DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger,
+ UserTracker userTracker) {
mContext = context;
mNoMan = mContext.getSystemService(NotificationManager.class);
mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
@@ -196,6 +198,7 @@
mDialogLaunchAnimator = dialogLaunchAnimator;
mUseSevereDialog = mContext.getResources().getBoolean(R.bool.config_severe_battery_dialog);
mUiEventLogger = uiEventLogger;
+ mUserTracker = userTracker;
}
@Override
@@ -692,7 +695,7 @@
Secure.putIntForUser(
resolver,
Secure.LOW_POWER_WARNING_ACKNOWLEDGED,
- 1, UserHandle.USER_CURRENT);
+ 1, mUserTracker.getUserId());
});
} else {
d.setTitle(R.string.battery_saver_confirmation_title);
@@ -843,7 +846,8 @@
logEvent(BatteryWarningEvents
.LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_SETTINGS);
dismissLowBatteryNotification();
- mContext.startActivityAsUser(mOpenBatterySaverSettings, UserHandle.CURRENT);
+ mContext.startActivityAsUser(mOpenBatterySaverSettings,
+ mUserTracker.getUserHandle());
} else if (action.equals(ACTION_START_SAVER)) {
logEvent(BatteryWarningEvents
.LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_TURN_ON);
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
index 2a6ca1a..8ad2f86 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -17,11 +17,11 @@
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
-import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import com.android.settingslib.Utils
import com.android.systemui.R
+import com.android.systemui.animation.LaunchableFrameLayout
import com.android.systemui.statusbar.events.BackgroundAnimatableView
class OngoingPrivacyChip @JvmOverloads constructor(
@@ -29,7 +29,7 @@
attrs: AttributeSet? = null,
defStyleAttrs: Int = 0,
defStyleRes: Int = 0
-) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
+) : LaunchableFrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
private var iconMargin = 0
private var iconSize = 0
diff --git a/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt b/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt
index b48ea23..be93550 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt
@@ -43,6 +43,7 @@
import javax.inject.Inject
private const val TAG = "AutoAddTracker"
+private const val DELIMITER = ","
/**
* Class to track tiles that have been auto-added
@@ -67,7 +68,7 @@
@GuardedBy("autoAdded")
private val autoAdded = ArraySet<String>()
- private var restoredTiles: Set<String>? = null
+ private var restoredTiles: Map<String, AutoTile>? = null
override val currentUserId: Int
get() = userId
@@ -98,25 +99,26 @@
when (intent.getStringExtra(Intent.EXTRA_SETTING_NAME)) {
Settings.Secure.QS_TILES -> {
restoredTiles = intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)
- ?.split(",")
- ?.toSet()
+ ?.split(DELIMITER)
+ ?.mapIndexed(::AutoTile)
+ ?.associateBy(AutoTile::tileType)
?: run {
Log.w(TAG, "Null restored tiles for user $userId")
- emptySet()
+ emptyMap()
}
}
Settings.Secure.QS_AUTO_ADDED_TILES -> {
- restoredTiles?.let { tiles ->
+ restoredTiles?.let { restoredTiles ->
val restoredAutoAdded = intent
.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)
- ?.split(",")
+ ?.split(DELIMITER)
?: emptyList()
val autoAddedBeforeRestore = intent
.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE)
- ?.split(",")
+ ?.split(DELIMITER)
?: emptyList()
- val tilesToRemove = restoredAutoAdded.filter { it !in tiles }
+ val tilesToRemove = restoredAutoAdded.filter { it !in restoredTiles }
if (tilesToRemove.isNotEmpty()) {
qsHost.removeTiles(tilesToRemove)
}
@@ -180,6 +182,9 @@
registerBroadcastReceiver()
}
+ fun getRestoredTilePosition(tile: String): Int =
+ restoredTiles?.get(tile)?.index ?: QSTileHost.POSITION_AT_END
+
/**
* Returns `true` if the tile has been auto-added before
*/
@@ -196,12 +201,12 @@
*/
fun setTileAdded(tile: String) {
val tiles = synchronized(autoAdded) {
- if (autoAdded.add(tile)) {
- getTilesFromListLocked()
- } else {
- null
- }
+ if (autoAdded.add(tile)) {
+ getTilesFromListLocked()
+ } else {
+ null
}
+ }
tiles?.let { saveTiles(it) }
}
@@ -222,7 +227,7 @@
}
private fun getTilesFromListLocked(): String {
- return TextUtils.join(",", autoAdded)
+ return TextUtils.join(DELIMITER, autoAdded)
}
private fun saveTiles(tiles: String) {
@@ -245,7 +250,7 @@
private fun getAdded(): Collection<String> {
val current = secureSettings.getStringForUser(Settings.Secure.QS_AUTO_ADDED_TILES, userId)
- return current?.split(",") ?: emptySet()
+ return current?.split(DELIMITER) ?: emptySet()
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -281,4 +286,6 @@
)
}
}
+
+ private data class AutoTile(val index: Int, val tileType: String)
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index f49ffb4..8ad102e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -49,6 +49,7 @@
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.ShadeInterpolation;
+import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.ui.MediaHost;
@@ -68,6 +69,7 @@
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.util.LifecycleFragment;
+import com.android.systemui.util.Utils;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -227,9 +229,7 @@
mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
this);
- LinearLayout footerActionsView = view.findViewById(R.id.qs_footer_actions);
- FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
- mListeningAndVisibilityLifecycleOwner);
+ bindFooterActionsView(view);
mFooterActionsController.init();
mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
@@ -290,6 +290,33 @@
});
}
+ private void bindFooterActionsView(View root) {
+ LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions);
+
+ if (!ComposeFacade.INSTANCE.isComposeAvailable()) {
+ Log.d(TAG, "Binding the View implementation of the QS footer actions");
+ FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
+ mListeningAndVisibilityLifecycleOwner);
+ return;
+ }
+
+ // Compose is available, so let's use the Compose implementation of the footer actions.
+ Log.d(TAG, "Binding the Compose implementation of the QS footer actions");
+ View composeView = ComposeFacade.INSTANCE.createFooterActionsView(root.getContext(),
+ mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner);
+
+ // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin
+ // to all views except for qs_footer_actions, so we set it to the Compose view.
+ composeView.setId(R.id.qs_footer_actions);
+
+ // Replace the View by the Compose provided one.
+ ViewGroup parent = (ViewGroup) footerActionsView.getParent();
+ ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams();
+ int index = parent.indexOfChild(footerActionsView);
+ parent.removeViewAt(index);
+ parent.addView(composeView, index, layoutParams);
+ }
+
@Override
public void setScrollListener(ScrollListener listener) {
mScrollListener = listener;
@@ -683,7 +710,7 @@
} else {
mQsMediaHost.setSquishFraction(mSquishinessFraction);
}
-
+ updateMediaPositions();
}
private void setAlphaAnimationProgress(float progress) {
@@ -758,6 +785,22 @@
- mQSPanelController.getPaddingBottom());
}
+ private void updateMediaPositions() {
+ if (Utils.useQsMediaPlayer(getContext())) {
+ View hostView = mQsMediaHost.getHostView();
+ // Make sure the media appears a bit from the top to make it look nicer
+ if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible()
+ && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) {
+ float interpolation = 1.0f - mLastQSExpansion;
+ interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation);
+ float translationY = -hostView.getHeight() * 1.3f * interpolation;
+ hostView.setTranslationY(translationY);
+ } else {
+ hostView.setTranslationY(0);
+ }
+ }
+ }
+
private boolean headerWillBeAnimating() {
return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
index 7cf63f6..1da30ad 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
@@ -36,7 +36,6 @@
void removeCallback(Callback callback);
void removeTile(String tileSpec);
void removeTiles(Collection<String> specs);
- void unmarkTileAsAutoAdded(String tileSpec);
int indexOf(String tileSpec);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 7bb672c..e85d0a3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -372,18 +372,18 @@
if (mUsingHorizontalLayout) {
// Only height remaining
parameters.getDisappearSize().set(0.0f, 0.4f);
- // Disappearing on the right side on the bottom
- parameters.getGonePivot().set(1.0f, 1.0f);
+ // Disappearing on the right side on the top
+ parameters.getGonePivot().set(1.0f, 0.0f);
// translating a bit horizontal
parameters.getContentTranslationFraction().set(0.25f, 1.0f);
parameters.setDisappearEnd(0.6f);
} else {
// Only width remaining
parameters.getDisappearSize().set(1.0f, 0.0f);
- // Disappearing on the bottom
- parameters.getGonePivot().set(0.0f, 1.0f);
+ // Disappearing on the top
+ parameters.getGonePivot().set(0.0f, 0.0f);
// translating a bit vertical
- parameters.getContentTranslationFraction().set(0.0f, 1.05f);
+ parameters.getContentTranslationFraction().set(0.0f, 1f);
parameters.setDisappearEnd(0.95f);
}
parameters.setFadeStartPosition(0.95f);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index cad296b..98af9df 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -314,7 +314,6 @@
if (!TILES_SETTING.equals(key)) {
return;
}
- Log.d(TAG, "Recreating tiles");
if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
}
@@ -327,6 +326,7 @@
}
}
if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
+ Log.d(TAG, "Recreating tiles: " + tileSpecs);
mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
tile -> {
Log.d(TAG, "Destroying tile: " + tile.getKey());
@@ -372,6 +372,8 @@
Log.d(TAG, "Destroying not available tile: " + tileSpec);
mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
}
+ } else {
+ Log.d(TAG, "No factory for a spec: " + tileSpec);
}
} catch (Throwable t) {
Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
@@ -427,11 +429,6 @@
mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs)));
}
- @Override
- public void unmarkTileAsAutoAdded(String spec) {
- if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec);
- }
-
/**
* Add a tile to the end
*
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
index 39d081d..36dc743 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
@@ -83,8 +83,7 @@
if (mListeners.size() > 0) {
mSecureSettings.unregisterContentObserver(mContentObserver);
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(
- Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED),
+ Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
false, mContentObserver, newUser);
}
}
@@ -100,8 +99,7 @@
mListeners.add(listener);
if (mListeners.size() == 1) {
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(
- Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED),
+ Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
false, mContentObserver, mUserTracker.getUserId());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index 79fcc7d..1712490 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -24,6 +24,7 @@
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toolbar;
@@ -74,8 +75,8 @@
toolbar.setNavigationIcon(
getResources().getDrawable(value.resourceId, mContext.getTheme()));
- toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
- mContext.getString(com.android.internal.R.string.reset));
+ toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
toolbar.setTitle(R.string.qs_edit);
mRecyclerView = findViewById(android.R.id.list);
mTransparentView = findViewById(R.id.customizer_transparent_view);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index cfda9fd..7c2536d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -15,7 +15,6 @@
*/
package com.android.systemui.qs.external;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
import android.app.PendingIntent;
@@ -63,6 +62,7 @@
import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.settings.DisplayTracker;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -90,6 +90,7 @@
private final TileServiceManager mServiceManager;
private final int mUser;
private final CustomTileStatePersister mCustomTileStatePersister;
+ private final DisplayTracker mDisplayTracker;
@Nullable
private android.graphics.drawable.Icon mDefaultIcon;
@Nullable
@@ -120,7 +121,8 @@
String action,
Context userContext,
CustomTileStatePersister customTileStatePersister,
- TileServices tileServices
+ TileServices tileServices,
+ DisplayTracker displayTracker
) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
@@ -135,6 +137,7 @@
mServiceManager = tileServices.getTileWrapper(this);
mService = mServiceManager.getTileService();
mCustomTileStatePersister = customTileStatePersister;
+ mDisplayTracker = displayTracker;
}
@Override
@@ -310,7 +313,7 @@
mIsShowingDialog = false;
try {
if (DEBUG) Log.d(TAG, "Removing token");
- mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
+ mWindowManager.removeWindowToken(mToken, mDisplayTracker.getDefaultDisplayId());
} catch (RemoteException e) {
}
}
@@ -335,7 +338,8 @@
if (mIsTokenGranted && !mIsShowingDialog) {
try {
if (DEBUG) Log.d(TAG, "Removing token");
- mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
+ mWindowManager.removeWindowToken(mToken,
+ mDisplayTracker.getDefaultDisplayId());
} catch (RemoteException e) {
}
mIsTokenGranted = false;
@@ -354,7 +358,7 @@
if (mIsTokenGranted) {
try {
if (DEBUG) Log.d(TAG, "Removing token");
- mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
+ mWindowManager.removeWindowToken(mToken, mDisplayTracker.getDefaultDisplayId());
} catch (RemoteException e) {
}
}
@@ -398,8 +402,8 @@
mViewClicked = view;
try {
if (DEBUG) Log.d(TAG, "Adding token");
- mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY,
- null /* options */);
+ mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG,
+ mDisplayTracker.getDefaultDisplayId(), null /* options */);
mIsTokenGranted = true;
} catch (RemoteException e) {
}
@@ -566,6 +570,7 @@
final QSLogger mQSLogger;
final CustomTileStatePersister mCustomTileStatePersister;
private TileServices mTileServices;
+ final DisplayTracker mDisplayTracker;
Context mUserContext;
String mSpec = "";
@@ -581,7 +586,8 @@
ActivityStarter activityStarter,
QSLogger qsLogger,
CustomTileStatePersister customTileStatePersister,
- TileServices tileServices
+ TileServices tileServices,
+ DisplayTracker displayTracker
) {
mQSHostLazy = hostLazy;
mBackgroundLooper = backgroundLooper;
@@ -593,6 +599,7 @@
mQSLogger = qsLogger;
mCustomTileStatePersister = customTileStatePersister;
mTileServices = tileServices;
+ mDisplayTracker = displayTracker;
}
Builder setSpec(@NonNull String spec) {
@@ -623,7 +630,8 @@
action,
mUserContext,
mCustomTileStatePersister,
- mTileServices
+ mTileServices,
+ mDisplayTracker
);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index 3d48fd1..84a18d8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -132,7 +132,7 @@
final String slot = tile.getComponent().getClassName();
// TileServices doesn't know how to add more than 1 icon per slot, so remove all
mMainHandler.post(() -> mHost.getIconController()
- .removeAllIconsForSlot(slot));
+ .removeAllIconsForExternalSlot(slot));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 30f8124..1921586 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -219,9 +219,9 @@
// Small button with the number only.
foregroundServicesWithTextView.isVisible = false
- foregroundServicesWithNumberView.visibility = View.VISIBLE
+ foregroundServicesWithNumberView.isVisible = true
foregroundServicesWithNumberView.setOnClickListener {
- foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+ foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView))
}
foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 9f376ae..d32ef32 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -49,109 +49,135 @@
}
fun logTileAdded(tileSpec: String) {
- log(DEBUG, {
- str1 = tileSpec
- }, {
- "[$str1] Tile added"
- })
+ buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" })
}
fun logTileDestroyed(tileSpec: String, reason: String) {
- log(DEBUG, {
- str1 = tileSpec
- str2 = reason
- }, {
- "[$str1] Tile destroyed. Reason: $str2"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ str2 = reason
+ },
+ { "[$str1] Tile destroyed. Reason: $str2" }
+ )
}
fun logTileChangeListening(tileSpec: String, listening: Boolean) {
- log(VERBOSE, {
- bool1 = listening
- str1 = tileSpec
- }, {
- "[$str1] Tile listening=$bool1"
- })
+ buffer.log(
+ TAG,
+ VERBOSE,
+ {
+ bool1 = listening
+ str1 = tileSpec
+ },
+ { "[$str1] Tile listening=$bool1" }
+ )
}
fun logAllTilesChangeListening(listening: Boolean, containerName: String, allSpecs: String) {
- log(DEBUG, {
- bool1 = listening
- str1 = containerName
- str2 = allSpecs
- }, {
- "Tiles listening=$bool1 in $str1. $str2"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ bool1 = listening
+ str1 = containerName
+ str2 = allSpecs
+ },
+ { "Tiles listening=$bool1 in $str1. $str2" }
+ )
}
fun logTileClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
- log(DEBUG, {
- str1 = tileSpec
- int1 = eventId
- str2 = StatusBarState.toString(statusBarState)
- str3 = toStateString(state)
- }, {
- "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ int1 = eventId
+ str2 = StatusBarState.toString(statusBarState)
+ str3 = toStateString(state)
+ },
+ { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" }
+ )
}
fun logHandleClick(tileSpec: String, eventId: Int) {
- log(DEBUG, {
- str1 = tileSpec
- int1 = eventId
- }, {
- "[$str1][$int1] Tile handling click."
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ int1 = eventId
+ },
+ { "[$str1][$int1] Tile handling click." }
+ )
}
fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
- log(DEBUG, {
- str1 = tileSpec
- int1 = eventId
- str2 = StatusBarState.toString(statusBarState)
- str3 = toStateString(state)
- }, {
- "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ int1 = eventId
+ str2 = StatusBarState.toString(statusBarState)
+ str3 = toStateString(state)
+ },
+ { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" }
+ )
}
fun logHandleSecondaryClick(tileSpec: String, eventId: Int) {
- log(DEBUG, {
- str1 = tileSpec
- int1 = eventId
- }, {
- "[$str1][$int1] Tile handling secondary click."
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ int1 = eventId
+ },
+ { "[$str1][$int1] Tile handling secondary click." }
+ )
}
fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) {
- log(DEBUG, {
- str1 = tileSpec
- int1 = eventId
- str2 = StatusBarState.toString(statusBarState)
- str3 = toStateString(state)
- }, {
- "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ int1 = eventId
+ str2 = StatusBarState.toString(statusBarState)
+ str3 = toStateString(state)
+ },
+ { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" }
+ )
}
fun logHandleLongClick(tileSpec: String, eventId: Int) {
- log(DEBUG, {
- str1 = tileSpec
- int1 = eventId
- }, {
- "[$str1][$int1] Tile handling long click."
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileSpec
+ int1 = eventId
+ },
+ { "[$str1][$int1] Tile handling long click." }
+ )
}
fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) {
- log(VERBOSE, {
- str1 = tileSpec
- int1 = lastType
- str2 = callback
- }, {
- "[$str1] mLastTileState=$int1, Callback=$str2."
- })
+ buffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = tileSpec
+ int1 = lastType
+ str2 = callback
+ },
+ { "[$str1] mLastTileState=$int1, Callback=$str2." }
+ )
}
// TODO(b/250618218): Remove this method once we know the root cause of b/250618218.
@@ -167,58 +193,75 @@
if (tileSpec != "internet") {
return
}
- log(VERBOSE, {
- str1 = tileSpec
- int1 = state
- bool1 = disabledByPolicy
- int2 = color
- }, {
- "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2."
- })
+ buffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = tileSpec
+ int1 = state
+ bool1 = disabledByPolicy
+ int2 = color
+ },
+ { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." }
+ )
}
fun logTileUpdated(tileSpec: String, state: QSTile.State) {
- log(VERBOSE, {
- str1 = tileSpec
- str2 = state.label?.toString()
- str3 = state.icon?.toString()
- int1 = state.state
- if (state is QSTile.SignalState) {
- bool1 = true
- bool2 = state.activityIn
- bool3 = state.activityOut
+ buffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = tileSpec
+ str2 = state.label?.toString()
+ str3 = state.icon?.toString()
+ int1 = state.state
+ if (state is QSTile.SignalState) {
+ bool1 = true
+ bool2 = state.activityIn
+ bool3 = state.activityOut
+ }
+ },
+ {
+ "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." +
+ if (bool1) " Activity in/out=$bool2/$bool3" else ""
}
- }, {
- "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." +
- if (bool1) " Activity in/out=$bool2/$bool3" else ""
- })
+ )
}
fun logPanelExpanded(expanded: Boolean, containerName: String) {
- log(DEBUG, {
- str1 = containerName
- bool1 = expanded
- }, {
- "$str1 expanded=$bool1"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = containerName
+ bool1 = expanded
+ },
+ { "$str1 expanded=$bool1" }
+ )
}
fun logOnViewAttached(orientation: Int, containerName: String) {
- log(DEBUG, {
- str1 = containerName
- int1 = orientation
- }, {
- "onViewAttached: $str1 orientation $int1"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = containerName
+ int1 = orientation
+ },
+ { "onViewAttached: $str1 orientation $int1" }
+ )
}
fun logOnViewDetached(orientation: Int, containerName: String) {
- log(DEBUG, {
- str1 = containerName
- int1 = orientation
- }, {
- "onViewDetached: $str1 orientation $int1"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = containerName
+ int1 = orientation
+ },
+ { "onViewDetached: $str1 orientation $int1" }
+ )
}
fun logOnConfigurationChanged(
@@ -226,13 +269,16 @@
newOrientation: Int,
containerName: String
) {
- log(DEBUG, {
- str1 = containerName
- int1 = lastOrientation
- int2 = newOrientation
- }, {
- "configuration change: $str1 orientation was $int1, now $int2"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = containerName
+ int1 = lastOrientation
+ int2 = newOrientation
+ },
+ { "configuration change: $str1 orientation was $int1, now $int2" }
+ )
}
fun logSwitchTileLayout(
@@ -241,32 +287,41 @@
force: Boolean,
containerName: String
) {
- log(DEBUG, {
- str1 = containerName
- bool1 = after
- bool2 = before
- bool3 = force
- }, {
- "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = containerName
+ bool1 = after
+ bool2 = before
+ bool3 = force
+ },
+ { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" }
+ )
}
fun logTileDistributionInProgress(tilesPerPageCount: Int, totalTilesCount: Int) {
- log(DEBUG, {
- int1 = tilesPerPageCount
- int2 = totalTilesCount
- }, {
- "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = tilesPerPageCount
+ int2 = totalTilesCount
+ },
+ { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" }
+ )
}
fun logTileDistributed(tileName: String, pageIndex: Int) {
- log(DEBUG, {
- str1 = tileName
- int1 = pageIndex
- }, {
- "Adding $str1 to page number $int1"
- })
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 = tileName
+ int1 = pageIndex
+ },
+ { "Adding $str1 to page number $int1" }
+ )
}
private fun toStateString(state: Int): String {
@@ -277,12 +332,4 @@
else -> "wrong state"
}
}
-
- private inline fun log(
- logLevel: LogLevel,
- initializer: LogMessage.() -> Unit,
- noinline printer: LogMessage.() -> String
- ) {
- buffer.log(TAG, logLevel, initializer, printer)
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index a92c7e3..24a4f60b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -33,7 +33,6 @@
import com.android.systemui.qs.tiles.BluetoothTile;
import com.android.systemui.qs.tiles.CameraToggleTile;
import com.android.systemui.qs.tiles.CastTile;
-import com.android.systemui.qs.tiles.CellularTile;
import com.android.systemui.qs.tiles.ColorCorrectionTile;
import com.android.systemui.qs.tiles.ColorInversionTile;
import com.android.systemui.qs.tiles.DataSaverTile;
@@ -54,7 +53,6 @@
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.qs.tiles.ScreenRecordTile;
import com.android.systemui.qs.tiles.UiModeNightTile;
-import com.android.systemui.qs.tiles.WifiTile;
import com.android.systemui.qs.tiles.WorkModeTile;
import com.android.systemui.util.leak.GarbageMonitor;
@@ -68,10 +66,8 @@
private static final String TAG = "QSFactory";
- private final Provider<WifiTile> mWifiTileProvider;
private final Provider<InternetTile> mInternetTileProvider;
private final Provider<BluetoothTile> mBluetoothTileProvider;
- private final Provider<CellularTile> mCellularTileProvider;
private final Provider<DndTile> mDndTileProvider;
private final Provider<ColorCorrectionTile> mColorCorrectionTileProvider;
private final Provider<ColorInversionTile> mColorInversionTileProvider;
@@ -106,10 +102,8 @@
public QSFactoryImpl(
Lazy<QSHost> qsHostLazy,
Provider<CustomTile.Builder> customTileBuilderProvider,
- Provider<WifiTile> wifiTileProvider,
Provider<InternetTile> internetTileProvider,
Provider<BluetoothTile> bluetoothTileProvider,
- Provider<CellularTile> cellularTileProvider,
Provider<DndTile> dndTileProvider,
Provider<ColorInversionTile> colorInversionTileProvider,
Provider<AirplaneModeTile> airplaneModeTileProvider,
@@ -139,10 +133,8 @@
mQsHostLazy = qsHostLazy;
mCustomTileBuilderProvider = customTileBuilderProvider;
- mWifiTileProvider = wifiTileProvider;
mInternetTileProvider = internetTileProvider;
mBluetoothTileProvider = bluetoothTileProvider;
- mCellularTileProvider = cellularTileProvider;
mDndTileProvider = dndTileProvider;
mColorInversionTileProvider = colorInversionTileProvider;
mAirplaneModeTileProvider = airplaneModeTileProvider;
@@ -186,14 +178,10 @@
protected QSTileImpl createTileInternal(String tileSpec) {
// Stock tiles.
switch (tileSpec) {
- case "wifi":
- return mWifiTileProvider.get();
case "internet":
return mInternetTileProvider.get();
case "bt":
return mBluetoothTileProvider.get();
- case "cell":
- return mCellularTileProvider.get();
case "dnd":
return mDndTileProvider.get();
case "inversion":
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index b355d4b..29d7fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -145,7 +145,6 @@
private val launchableViewDelegate = LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
- superSetTransitionVisibility = { super.setTransitionVisibility(it) },
)
private var lastDisabledByPolicy = false
@@ -362,10 +361,6 @@
launchableViewDelegate.setVisibility(visibility)
}
- override fun setTransitionVisibility(visibility: Int) {
- launchableViewDelegate.setTransitionVisibility(visibility)
- }
-
// Accessibility
override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
deleted file mode 100644
index 04a25fc..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * Copyright (C) 2014 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;
-
-import static com.android.systemui.Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA;
-
-import android.annotation.NonNull;
-import android.app.AlertDialog;
-import android.app.AlertDialog.Builder;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.provider.Settings;
-import android.service.quicksettings.Tile;
-import android.telephony.SubscriptionManager;
-import android.text.Html;
-import android.text.TextUtils;
-import android.view.View;
-import android.view.WindowManager.LayoutParams;
-import android.widget.Switch;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.settingslib.net.DataUsageController;
-import com.android.systemui.Prefs;
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSIconView;
-import com.android.systemui.plugins.qs.QSTile.SignalState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.SignalTileView;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.connectivity.IconState;
-import com.android.systemui.statusbar.connectivity.MobileDataIndicators;
-import com.android.systemui.statusbar.connectivity.NetworkController;
-import com.android.systemui.statusbar.connectivity.SignalCallback;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import javax.inject.Inject;
-
-/** Quick settings tile: Cellular **/
-public class CellularTile extends QSTileImpl<SignalState> {
- private static final String ENABLE_SETTINGS_DATA_PLAN = "enable.settings.data.plan";
-
- private final NetworkController mController;
- private final DataUsageController mDataController;
- private final KeyguardStateController mKeyguard;
- private final CellSignalCallback mSignalCallback = new CellSignalCallback();
-
- @Inject
- public CellularTile(
- QSHost host,
- @Background Looper backgroundLooper,
- @Main Handler mainHandler,
- FalsingManager falsingManager,
- MetricsLogger metricsLogger,
- StatusBarStateController statusBarStateController,
- ActivityStarter activityStarter,
- QSLogger qsLogger,
- NetworkController networkController,
- KeyguardStateController keyguardStateController
-
- ) {
- super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
- statusBarStateController, activityStarter, qsLogger);
- mController = networkController;
- mKeyguard = keyguardStateController;
- mDataController = mController.getMobileDataController();
- mController.observe(getLifecycle(), mSignalCallback);
- }
-
- @Override
- public SignalState newTileState() {
- return new SignalState();
- }
-
- @Override
- public QSIconView createTileView(Context context) {
- return new SignalTileView(context);
- }
-
- @Override
- public Intent getLongClickIntent() {
- if (getState().state == Tile.STATE_UNAVAILABLE) {
- return new Intent(Settings.ACTION_WIRELESS_SETTINGS);
- }
- return getCellularSettingIntent();
- }
-
- @Override
- protected void handleClick(@Nullable View view) {
- if (getState().state == Tile.STATE_UNAVAILABLE) {
- return;
- }
- if (mDataController.isMobileDataEnabled()) {
- maybeShowDisableDialog();
- } else {
- mDataController.setMobileDataEnabled(true);
- }
- }
-
- private void maybeShowDisableDialog() {
- if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) {
- // Directly turn off mobile data if the user has seen the dialog before.
- mDataController.setMobileDataEnabled(false);
- return;
- }
- String carrierName = mController.getMobileDataNetworkName();
- boolean isInService = mController.isMobileDataNetworkInService();
- if (TextUtils.isEmpty(carrierName) || !isInService) {
- carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier);
- }
- AlertDialog dialog = new Builder(mContext)
- .setTitle(R.string.mobile_data_disable_title)
- .setMessage(mContext.getString(R.string.mobile_data_disable_message, carrierName))
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(
- com.android.internal.R.string.alert_windows_notification_turn_off_action,
- (d, w) -> {
- mDataController.setMobileDataEnabled(false);
- Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true);
- })
- .create();
- dialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG);
- SystemUIDialog.setShowForAllUsers(dialog, true);
- SystemUIDialog.registerDismissListener(dialog);
- SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing());
- dialog.show();
- }
-
- @Override
- protected void handleSecondaryClick(@Nullable View view) {
- handleLongClick(view);
- }
-
- @Override
- public CharSequence getTileLabel() {
- return mContext.getString(R.string.quick_settings_cellular_detail_title);
- }
-
- @Override
- protected void handleUpdateState(SignalState state, Object arg) {
- CallbackInfo cb = (CallbackInfo) arg;
- if (cb == null) {
- cb = mSignalCallback.mInfo;
- }
-
- final Resources r = mContext.getResources();
- state.label = r.getString(R.string.mobile_data);
- boolean mobileDataEnabled = mDataController.isMobileDataSupported()
- && mDataController.isMobileDataEnabled();
- state.value = mobileDataEnabled;
- state.activityIn = mobileDataEnabled && cb.activityIn;
- state.activityOut = mobileDataEnabled && cb.activityOut;
- state.expandedAccessibilityClassName = Switch.class.getName();
- if (cb.noSim) {
- state.icon = ResourceIcon.get(R.drawable.ic_qs_no_sim);
- } else {
- state.icon = ResourceIcon.get(R.drawable.ic_swap_vert);
- }
-
- if (cb.noSim) {
- state.state = Tile.STATE_UNAVAILABLE;
- state.secondaryLabel = r.getString(R.string.keyguard_missing_sim_message_short);
- } else if (cb.airplaneModeEnabled) {
- state.state = Tile.STATE_UNAVAILABLE;
- state.secondaryLabel = r.getString(R.string.status_bar_airplane);
- } else if (mobileDataEnabled) {
- state.state = Tile.STATE_ACTIVE;
- state.secondaryLabel = appendMobileDataType(
- // Only show carrier name if there are more than 1 subscription
- cb.multipleSubs ? cb.dataSubscriptionName : "",
- getMobileDataContentName(cb));
- } else {
- state.state = Tile.STATE_INACTIVE;
- state.secondaryLabel = r.getString(R.string.cell_data_off);
- }
-
- state.contentDescription = state.label;
- if (state.state == Tile.STATE_INACTIVE) {
- // This information is appended later by converting the Tile.STATE_INACTIVE state.
- state.stateDescription = "";
- } else {
- state.stateDescription = state.secondaryLabel;
- }
- }
-
- private CharSequence appendMobileDataType(CharSequence current, CharSequence dataType) {
- if (TextUtils.isEmpty(dataType)) {
- return Html.fromHtml(current.toString(), 0);
- }
- if (TextUtils.isEmpty(current)) {
- return Html.fromHtml(dataType.toString(), 0);
- }
- String concat = mContext.getString(R.string.mobile_carrier_text_format, current, dataType);
- return Html.fromHtml(concat, 0);
- }
-
- private CharSequence getMobileDataContentName(CallbackInfo cb) {
- if (cb.roaming && !TextUtils.isEmpty(cb.dataContentDescription)) {
- String roaming = mContext.getString(R.string.data_connection_roaming);
- String dataDescription = cb.dataContentDescription.toString();
- return mContext.getString(R.string.mobile_data_text_format, roaming, dataDescription);
- }
- if (cb.roaming) {
- return mContext.getString(R.string.data_connection_roaming);
- }
- return cb.dataContentDescription;
- }
-
- @Override
- public int getMetricsCategory() {
- return MetricsEvent.QS_CELLULAR;
- }
-
- @Override
- public boolean isAvailable() {
- return mController.hasMobileDataFeature()
- && mHost.getUserContext().getUserId() == UserHandle.USER_SYSTEM;
- }
-
- private static final class CallbackInfo {
- boolean airplaneModeEnabled;
- @Nullable
- CharSequence dataSubscriptionName;
- @Nullable
- CharSequence dataContentDescription;
- boolean activityIn;
- boolean activityOut;
- boolean noSim;
- boolean roaming;
- boolean multipleSubs;
- }
-
- private final class CellSignalCallback implements SignalCallback {
- private final CallbackInfo mInfo = new CallbackInfo();
-
- @Override
- public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
- if (indicators.qsIcon == null) {
- // Not data sim, don't display.
- return;
- }
- mInfo.dataSubscriptionName = mController.getMobileDataNetworkName();
- mInfo.dataContentDescription = indicators.qsDescription != null
- ? indicators.typeContentDescriptionHtml : null;
- mInfo.activityIn = indicators.activityIn;
- mInfo.activityOut = indicators.activityOut;
- mInfo.roaming = indicators.roaming;
- mInfo.multipleSubs = mController.getNumberSubscriptions() > 1;
- refreshState(mInfo);
- }
-
- @Override
- public void setNoSims(boolean show, boolean simDetected) {
- mInfo.noSim = show;
- refreshState(mInfo);
- }
-
- @Override
- public void setIsAirplaneMode(@NonNull IconState icon) {
- mInfo.airplaneModeEnabled = icon.visible;
- refreshState(mInfo);
- }
- }
-
- static Intent getCellularSettingIntent() {
- Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);
- int dataSub = SubscriptionManager.getDefaultDataSubscriptionId();
- if (dataSub != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
- intent.putExtra(Settings.EXTRA_SUB_ID,
- SubscriptionManager.getDefaultDataSubscriptionId());
- }
- return intent;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 51de522..b155e13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -255,17 +255,19 @@
Log.d(TAG, "setWifiIndicators: " + indicators);
}
mWifiInfo.mEnabled = indicators.enabled;
- if (indicators.qsIcon == null) {
- return;
- }
- mWifiInfo.mConnected = indicators.qsIcon.visible;
- mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
- mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
- mWifiInfo.mEnabled = indicators.enabled;
mWifiInfo.mSsid = indicators.description;
mWifiInfo.mIsTransient = indicators.isTransient;
mWifiInfo.mStatusLabel = indicators.statusLabel;
- refreshState(mWifiInfo);
+ if (indicators.qsIcon != null) {
+ mWifiInfo.mConnected = indicators.qsIcon.visible;
+ mWifiInfo.mWifiSignalIconId = indicators.qsIcon.icon;
+ mWifiInfo.mWifiSignalContentDescription = indicators.qsIcon.contentDescription;
+ refreshState(mWifiInfo);
+ } else {
+ mWifiInfo.mConnected = false;
+ mWifiInfo.mWifiSignalIconId = 0;
+ mWifiInfo.mWifiSignalContentDescription = null;
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 57a00c9..b6b657e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -204,15 +204,6 @@
Trace.endSection();
}
- @Override
- public void onUserListItemClicked(@NonNull UserRecord record,
- @Nullable UserSwitchDialogController.DialogShower dialogShower) {
- if (dialogShower != null) {
- mDialogShower.dismiss();
- }
- super.onUserListItemClicked(record, dialogShower);
- }
-
public void linkToViewGroup(ViewGroup viewGroup) {
PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
deleted file mode 100644
index b2be56cc..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * Copyright (C) 2014 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;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
-import android.service.quicksettings.Tile;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.widget.Switch;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSIconView;
-import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.plugins.qs.QSTile.SignalState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.AlphaControlledSignalTileView;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.tileimpl.QSIconViewImpl;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.statusbar.connectivity.AccessPointController;
-import com.android.systemui.statusbar.connectivity.NetworkController;
-import com.android.systemui.statusbar.connectivity.SignalCallback;
-import com.android.systemui.statusbar.connectivity.WifiIcons;
-import com.android.systemui.statusbar.connectivity.WifiIndicators;
-
-import javax.inject.Inject;
-
-/** Quick settings tile: Wifi **/
-public class WifiTile extends QSTileImpl<SignalState> {
- private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS);
-
- protected final NetworkController mController;
- private final AccessPointController mWifiController;
- private final QSTile.SignalState mStateBeforeClick = newTileState();
-
- protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback();
- private boolean mExpectDisabled;
-
- @Inject
- public WifiTile(
- QSHost host,
- @Background Looper backgroundLooper,
- @Main Handler mainHandler,
- FalsingManager falsingManager,
- MetricsLogger metricsLogger,
- StatusBarStateController statusBarStateController,
- ActivityStarter activityStarter,
- QSLogger qsLogger,
- NetworkController networkController,
- AccessPointController accessPointController
- ) {
- super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
- statusBarStateController, activityStarter, qsLogger);
- mController = networkController;
- mWifiController = accessPointController;
- mController.observe(getLifecycle(), mSignalCallback);
- mStateBeforeClick.spec = "wifi";
- }
-
- @Override
- public SignalState newTileState() {
- return new SignalState();
- }
-
- @Override
- public QSIconView createTileView(Context context) {
- return new AlphaControlledSignalTileView(context);
- }
-
- @Override
- public Intent getLongClickIntent() {
- return WIFI_SETTINGS;
- }
-
- @Override
- protected void handleClick(@Nullable View view) {
- // Secondary clicks are header clicks, just toggle.
- mState.copyTo(mStateBeforeClick);
- boolean wifiEnabled = mState.value;
- // Immediately enter transient state when turning on wifi.
- refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
- mController.setWifiEnabled(!wifiEnabled);
- mExpectDisabled = wifiEnabled;
- if (mExpectDisabled) {
- mHandler.postDelayed(() -> {
- if (mExpectDisabled) {
- mExpectDisabled = false;
- refreshState();
- }
- }, QSIconViewImpl.QS_ANIM_LENGTH);
- }
- }
-
- @Override
- protected void handleSecondaryClick(@Nullable View view) {
- if (!mWifiController.canConfigWifi()) {
- mActivityStarter.postStartActivityDismissingKeyguard(
- new Intent(Settings.ACTION_WIFI_SETTINGS), 0);
- return;
- }
- if (!mState.value) {
- mController.setWifiEnabled(true);
- }
- }
-
- @Override
- public CharSequence getTileLabel() {
- return mContext.getString(R.string.quick_settings_wifi_label);
- }
-
- @Override
- protected void handleUpdateState(SignalState state, Object arg) {
- if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg);
- final CallbackInfo cb = mSignalCallback.mInfo;
- if (mExpectDisabled) {
- if (cb.enabled) {
- return; // Ignore updates until disabled event occurs.
- } else {
- mExpectDisabled = false;
- }
- }
- boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
- boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0)
- && (cb.ssid != null || cb.wifiSignalIconId != WifiIcons.QS_WIFI_NO_NETWORK);
- boolean wifiNotConnected = (cb.ssid == null)
- && (cb.wifiSignalIconId == WifiIcons.QS_WIFI_NO_NETWORK);
- if (state.slash == null) {
- state.slash = new SlashState();
- state.slash.rotation = 6;
- }
- state.slash.isSlashed = false;
- boolean isTransient = transientEnabling || cb.isTransient;
- state.secondaryLabel = getSecondaryLabel(isTransient, cb.statusLabel);
- state.state = Tile.STATE_ACTIVE;
- state.dualTarget = true;
- state.value = transientEnabling || cb.enabled;
- state.activityIn = cb.enabled && cb.activityIn;
- state.activityOut = cb.enabled && cb.activityOut;
- final StringBuffer minimalContentDescription = new StringBuffer();
- final StringBuffer minimalStateDescription = new StringBuffer();
- final Resources r = mContext.getResources();
- if (isTransient) {
- state.icon = ResourceIcon.get(
- com.android.internal.R.drawable.ic_signal_wifi_transient_animation);
- state.label = r.getString(R.string.quick_settings_wifi_label);
- } else if (!state.value) {
- state.slash.isSlashed = true;
- state.state = Tile.STATE_INACTIVE;
- state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_DISABLED);
- state.label = r.getString(R.string.quick_settings_wifi_label);
- } else if (wifiConnected) {
- state.icon = ResourceIcon.get(cb.wifiSignalIconId);
- state.label = cb.ssid != null ? removeDoubleQuotes(cb.ssid) : getTileLabel();
- } else if (wifiNotConnected) {
- state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK);
- state.label = r.getString(R.string.quick_settings_wifi_label);
- } else {
- state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK);
- state.label = r.getString(R.string.quick_settings_wifi_label);
- }
- minimalContentDescription.append(
- mContext.getString(R.string.quick_settings_wifi_label)).append(",");
- if (state.value) {
- if (wifiConnected) {
- minimalStateDescription.append(cb.wifiSignalContentDescription);
- minimalContentDescription.append(removeDoubleQuotes(cb.ssid));
- if (!TextUtils.isEmpty(state.secondaryLabel)) {
- minimalContentDescription.append(",").append(state.secondaryLabel);
- }
- }
- }
- state.stateDescription = minimalStateDescription.toString();
- state.contentDescription = minimalContentDescription.toString();
- state.dualLabelContentDescription = r.getString(
- R.string.accessibility_quick_settings_open_settings, getTileLabel());
- state.expandedAccessibilityClassName = Switch.class.getName();
- }
-
- private CharSequence getSecondaryLabel(boolean isTransient, String statusLabel) {
- return isTransient
- ? mContext.getString(R.string.quick_settings_wifi_secondary_label_transient)
- : statusLabel;
- }
-
- @Override
- public int getMetricsCategory() {
- return MetricsEvent.QS_WIFI;
- }
-
- @Override
- public boolean isAvailable() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI);
- }
-
- @Nullable
- private static String removeDoubleQuotes(String string) {
- if (string == null) return null;
- final int length = string.length();
- if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
- return string.substring(1, length - 1);
- }
- return string;
- }
-
- protected static final class CallbackInfo {
- boolean enabled;
- boolean connected;
- int wifiSignalIconId;
- @Nullable
- String ssid;
- boolean activityIn;
- boolean activityOut;
- @Nullable
- String wifiSignalContentDescription;
- boolean isTransient;
- @Nullable
- public String statusLabel;
-
- @Override
- public String toString() {
- return new StringBuilder("CallbackInfo[")
- .append("enabled=").append(enabled)
- .append(",connected=").append(connected)
- .append(",wifiSignalIconId=").append(wifiSignalIconId)
- .append(",ssid=").append(ssid)
- .append(",activityIn=").append(activityIn)
- .append(",activityOut=").append(activityOut)
- .append(",wifiSignalContentDescription=").append(wifiSignalContentDescription)
- .append(",isTransient=").append(isTransient)
- .append(']').toString();
- }
- }
-
- protected final class WifiSignalCallback implements SignalCallback {
- final CallbackInfo mInfo = new CallbackInfo();
-
- @Override
- public void setWifiIndicators(@NonNull WifiIndicators indicators) {
- if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + indicators.enabled);
- if (indicators.qsIcon == null) {
- return;
- }
- mInfo.enabled = indicators.enabled;
- mInfo.connected = indicators.qsIcon.visible;
- mInfo.wifiSignalIconId = indicators.qsIcon.icon;
- mInfo.ssid = indicators.description;
- mInfo.activityIn = indicators.activityIn;
- mInfo.activityOut = indicators.activityOut;
- mInfo.wifiSignalContentDescription = indicators.qsIcon.contentDescription;
- mInfo.isTransient = indicators.isTransient;
- mInfo.statusLabel = indicators.statusLabel;
- refreshState();
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index a6c7781..72c6bfe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -101,7 +101,6 @@
@MainThread
public void onManagedProfileRemoved() {
mHost.removeTile(getTileSpec());
- mHost.unmarkTileAsAutoAdded(getTileSpec());
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index 314252b..4c9c99c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -36,6 +36,7 @@
import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.qs.tiles.UserDetailView
import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.user.ui.dialog.DialogShowerImpl
import javax.inject.Inject
import javax.inject.Provider
@@ -130,19 +131,6 @@
}
}
- private class DialogShowerImpl(
- private val animateFrom: Dialog,
- private val dialogLaunchAnimator: DialogLaunchAnimator
- ) : DialogInterface by animateFrom, DialogShower {
- override fun showDialog(dialog: Dialog, cuj: DialogCuj) {
- dialogLaunchAnimator.showFromDialog(
- dialog,
- animateFrom = animateFrom,
- cuj
- )
- }
- }
-
interface DialogShower : DialogInterface {
fun showDialog(dialog: Dialog, cuj: DialogCuj)
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 4d005be..a979e5a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -17,11 +17,9 @@
package com.android.systemui.recents;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
-import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
@@ -31,6 +29,7 @@
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_WINDOW_CORNER_RADIUS;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
@@ -44,8 +43,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
-import android.graphics.Insets;
-import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Binder;
@@ -77,6 +74,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
@@ -90,11 +88,11 @@
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.buttons.KeyButtonView;
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -147,6 +145,7 @@
private final UserTracker mUserTracker;
private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
private final UiEventLogger mUiEventLogger;
+ private final DisplayTracker mDisplayTracker;
private Region mActiveNavBarRegion;
private SurfaceControl mNavigationBarSurface;
@@ -228,11 +227,11 @@
@Override
public void onImeSwitcherPressed() {
- // TODO(b/204901476) We're intentionally using DEFAULT_DISPLAY for now since
+ // TODO(b/204901476) We're intentionally using the default display for now since
// Launcher/Taskbar isn't display aware.
mContext.getSystemService(InputMethodManager.class)
.showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
- DEFAULT_DISPLAY);
+ mDisplayTracker.getDefaultDisplayId());
mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
}
@@ -311,7 +310,7 @@
intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(intent, mUserTracker.getUserHandle());
});
}
@@ -322,18 +321,8 @@
}
@Override
- public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
- Insets visibleInsets, Task.TaskKey task) {
- mScreenshotHelper.provideScreenshot(
- screenImageBundle,
- locationInScreen,
- visibleInsets,
- task.id,
- task.userId,
- task.sourceComponent,
- SCREENSHOT_OVERVIEW,
- mHandler,
- null);
+ public void takeScreenshot(ScreenshotRequest request) {
+ mScreenshotHelper.takeScreenshot(request, mHandler, null);
}
@Override
@@ -520,6 +509,7 @@
UserTracker userTracker,
ScreenLifecycle screenLifecycle,
UiEventLogger uiEventLogger,
+ DisplayTracker displayTracker,
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
AssistUtils assistUtils,
DumpManager dumpManager) {
@@ -547,6 +537,7 @@
mSysUiState = sysUiState;
mSysUiState.addCallback(this::notifySystemUiStateFlags);
mUiEventLogger = uiEventLogger;
+ mDisplayTracker = displayTracker;
dumpManager.registerDumpable(getClass().getSimpleName(), this);
@@ -665,13 +656,14 @@
}
private void onStatusBarStateChanged(boolean keyguardShowing, boolean keyguardOccluded,
- boolean bouncerShowing, boolean isDozing, boolean panelExpanded) {
+ boolean bouncerShowing, boolean isDozing, boolean panelExpanded, boolean isDreaming) {
mSysUiState.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
keyguardShowing && !keyguardOccluded)
.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED,
keyguardShowing && keyguardOccluded)
.setFlag(SYSUI_STATE_BOUNCER_SHOWING, bouncerShowing)
.setFlag(SYSUI_STATE_DEVICE_DOZING, isDozing)
+ .setFlag(SYSUI_STATE_DEVICE_DREAMING, isDreaming)
.commitUpdate(mContext.getDisplayId());
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 44b18ec..68e3dcd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -23,6 +23,7 @@
import android.os.Handler
import android.os.Looper
import android.os.ResultReceiver
+import android.os.UserHandle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
@@ -77,6 +78,14 @@
MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
CaptureTargetResultReceiver()
)
+
+ // Send SystemUI's user handle as the host app user handle because SystemUI
+ // is the 'host app' (the app that receives screen capture data)
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+ UserHandle.of(UserHandle.myUserId())
+ )
+
val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!)
if (animationController == null) {
dismiss()
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
index 017e57f..310baaf 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
@@ -25,8 +25,22 @@
import com.android.systemui.R
object ActionIntentCreator {
+ /** @return a chooser intent to share the given URI. */
+ fun createShareIntent(uri: Uri) = createShareIntent(uri, null, null)
+
/** @return a chooser intent to share the given URI with the optional provided subject. */
- fun createShareIntent(uri: Uri, subject: String?): Intent {
+ fun createShareIntentWithSubject(uri: Uri, subject: String?) =
+ createShareIntent(uri, subject = subject)
+
+ /** @return a chooser intent to share the given URI with the optional provided extra text. */
+ fun createShareIntentWithExtraText(uri: Uri, extraText: String?) =
+ createShareIntent(uri, extraText = extraText)
+
+ private fun createShareIntent(
+ uri: Uri,
+ subject: String? = null,
+ extraText: String? = null
+ ): Intent {
// Create a share intent, this will always go through the chooser activity first
// which should not trigger auto-enter PiP
val sharingIntent =
@@ -43,6 +57,7 @@
)
putExtra(Intent.EXTRA_SUBJECT, subject)
+ putExtra(Intent.EXTRA_TEXT, extraText)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index 01e32b7a..aa8e2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -22,7 +22,6 @@
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log
-import android.view.Display
import android.view.IRemoteAnimationFinishedCallback
import android.view.IRemoteAnimationRunner
import android.view.RemoteAnimationAdapter
@@ -33,6 +32,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.DisplayTracker
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
@@ -47,6 +47,7 @@
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
private val context: Context,
+ private val displayTracker: DisplayTracker
) {
/**
* Execute the given intent with startActivity while performing operations for screenshot action
@@ -82,7 +83,7 @@
val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
try {
WindowManagerGlobal.getWindowManagerService()
- .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
+ .overridePendingAppTransitionRemote(runner, displayTracker.defaultDisplayId)
} catch (e: Exception) {
Log.e(TAG, "Error overriding screenshot app transition", e)
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java
index 814b8e9..4f5cb72 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java
@@ -16,8 +16,6 @@
package com.android.systemui.screenshot;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_EDIT;
import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE;
import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT;
@@ -35,6 +33,7 @@
import android.view.RemoteAnimationAdapter;
import android.view.WindowManagerGlobal;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -52,14 +51,17 @@
private final CentralSurfaces mCentralSurfaces;
private final ActivityManagerWrapper mActivityManagerWrapper;
private final ScreenshotSmartActions mScreenshotSmartActions;
+ private final DisplayTracker mDisplayTracker;
@Inject
public ActionProxyReceiver(Optional<CentralSurfaces> centralSurfacesOptional,
ActivityManagerWrapper activityManagerWrapper,
- ScreenshotSmartActions screenshotSmartActions) {
+ ScreenshotSmartActions screenshotSmartActions,
+ DisplayTracker displayTracker) {
mCentralSurfaces = centralSurfacesOptional.orElse(null);
mActivityManagerWrapper = activityManagerWrapper;
mScreenshotSmartActions = screenshotSmartActions;
+ mDisplayTracker = displayTracker;
}
@Override
@@ -78,7 +80,8 @@
ScreenshotController.SCREENSHOT_REMOTE_RUNNER, 0, 0);
try {
WindowManagerGlobal.getWindowManagerService()
- .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
+ .overridePendingAppTransitionRemote(runner,
+ mDisplayTracker.getDefaultDisplayId());
} catch (Exception e) {
Log.e(TAG, "Error overriding screenshot app transition", e);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
new file mode 100644
index 0000000..146e576
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
+import android.app.IAssistDataReceiver;
+import android.app.assist.AssistContent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/**
+ * Can be used to request the AssistContent from a provided task id, useful for getting the web uri
+ * if provided from the task.
+ *
+ * Forked from
+ * packages/apps/Launcher3/quickstep/src/com/android/quickstep/util/AssistContentRequester.java
+ */
+@SysUISingleton
+public class AssistContentRequester {
+ private static final String TAG = "AssistContentRequester";
+ private static final String ASSIST_KEY_CONTENT = "content";
+
+ /** For receiving content, called on the main thread. */
+ public interface Callback {
+ /**
+ * Called when the {@link android.app.assist.AssistContent} of the requested task is
+ * available.
+ **/
+ void onAssistContentAvailable(AssistContent assistContent);
+ }
+
+ private final IActivityTaskManager mActivityTaskManager;
+ private final String mPackageName;
+ private final Executor mCallbackExecutor;
+ private final Executor mSystemInteractionExecutor;
+
+ // If system loses the callback, our internal cache of original callback will also get cleared.
+ private final Map<Object, Callback> mPendingCallbacks =
+ Collections.synchronizedMap(new WeakHashMap<>());
+
+ @Inject
+ public AssistContentRequester(Context context, @Main Executor mainExecutor,
+ @Background Executor bgExecutor) {
+ mActivityTaskManager = ActivityTaskManager.getService();
+ mPackageName = context.getApplicationContext().getPackageName();
+ mCallbackExecutor = mainExecutor;
+ mSystemInteractionExecutor = bgExecutor;
+ }
+
+ /**
+ * Request the {@link AssistContent} from the task with the provided id.
+ *
+ * @param taskId to query for the content.
+ * @param callback to call when the content is available, called on the main thread.
+ */
+ public void requestAssistContent(final int taskId, final Callback callback) {
+ // ActivityTaskManager interaction here is synchronous, so call off the main thread.
+ mSystemInteractionExecutor.execute(() -> {
+ try {
+ mActivityTaskManager.requestAssistDataForTask(
+ new AssistDataReceiver(callback, this), taskId, mPackageName);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Requesting assist content failed for task: " + taskId, e);
+ }
+ });
+ }
+
+ private void executeOnMainExecutor(Runnable callback) {
+ mCallbackExecutor.execute(callback);
+ }
+
+ private static final class AssistDataReceiver extends IAssistDataReceiver.Stub {
+
+ // The AssistDataReceiver binder callback object is passed to a system server, that may
+ // keep hold of it for longer than the lifetime of the AssistContentRequester object,
+ // potentially causing a memory leak. In the callback passed to the system server, only
+ // keep a weak reference to the parent object and lookup its callback if it still exists.
+ private final WeakReference<AssistContentRequester> mParentRef;
+ private final Object mCallbackKey = new Object();
+
+ AssistDataReceiver(Callback callback, AssistContentRequester parent) {
+ parent.mPendingCallbacks.put(mCallbackKey, callback);
+ mParentRef = new WeakReference<>(parent);
+ }
+
+ @Override
+ public void onHandleAssistData(Bundle data) {
+ if (data == null) {
+ return;
+ }
+
+ final AssistContent content = data.getParcelable(ASSIST_KEY_CONTENT);
+ if (content == null) {
+ Log.e(TAG, "Received AssistData, but no AssistContent found");
+ return;
+ }
+
+ AssistContentRequester requester = mParentRef.get();
+ if (requester != null) {
+ Callback callback = requester.mPendingCallbacks.get(mCallbackKey);
+ if (callback != null) {
+ requester.executeOnMainExecutor(
+ () -> callback.onAssistContentAvailable(content));
+ } else {
+ Log.d(TAG, "Callback received after calling UI was disposed of");
+ }
+ } else {
+ Log.d(TAG, "Callback received after Requester was collected");
+ }
+ }
+
+ @Override
+ public void onHandleAssistScreenshot(Bitmap screenshot) {}
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 5450db9..ca8e101 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -49,6 +49,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
+import com.android.systemui.settings.UserTracker;
import com.google.common.util.concurrent.ListenableFuture;
@@ -79,6 +80,7 @@
private final LongScreenshotData mLongScreenshotHolder;
private final ActionIntentExecutor mActionExecutor;
private final FeatureFlags mFeatureFlags;
+ private final UserTracker mUserTracker;
private ImageView mPreview;
private ImageView mTransitionView;
@@ -110,7 +112,7 @@
public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter,
@Main Executor mainExecutor, @Background Executor bgExecutor,
LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags, UserTracker userTracker) {
mUiEventLogger = uiEventLogger;
mUiExecutor = mainExecutor;
mBackgroundExecutor = bgExecutor;
@@ -118,6 +120,7 @@
mLongScreenshotHolder = longScreenshotHolder;
mActionExecutor = actionExecutor;
mFeatureFlags = featureFlags;
+ mUserTracker = userTracker;
}
@@ -363,7 +366,7 @@
private void doShare(Uri uri) {
if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
- Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri, null);
+ Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri);
mActionExecutor.launchIntentAsync(shareIntent, null,
mScreenshotUserHandle.getIdentifier(), false);
} else {
@@ -375,7 +378,7 @@
Intent sharingChooserIntent = Intent.createChooser(intent, null)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
+ startActivityAsUser(sharingChooserIntent, mUserTracker.getUserHandle());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
new file mode 100644
index 0000000..ad66514
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -0,0 +1,145 @@
+package com.android.systemui.screenshot
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.os.UserHandle
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import android.view.ViewTreeObserver
+import android.view.animation.AccelerateDecelerateInterpolator
+import androidx.constraintlayout.widget.Guideline
+import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+/**
+ * MessageContainerController controls the display of content in the screenshot message container.
+ */
+class MessageContainerController
+@Inject
+constructor(
+ private val workProfileMessageController: WorkProfileMessageController,
+ private val screenshotDetectionController: ScreenshotDetectionController,
+ private val featureFlags: FeatureFlags,
+) {
+ private lateinit var container: ViewGroup
+ private lateinit var guideline: Guideline
+ private lateinit var workProfileFirstRunView: ViewGroup
+ private lateinit var detectionNoticeView: ViewGroup
+ private var animateOut: Animator? = null
+
+ fun setView(screenshotView: ViewGroup) {
+ container = screenshotView.requireViewById(R.id.screenshot_message_container)
+ guideline = screenshotView.requireViewById(R.id.guideline)
+
+ workProfileFirstRunView = container.requireViewById(R.id.work_profile_first_run)
+ detectionNoticeView = container.requireViewById(R.id.screenshot_detection_notice)
+
+ // Restore to starting state.
+ container.visibility = View.GONE
+ guideline.setGuidelineEnd(0)
+ workProfileFirstRunView.visibility = View.GONE
+ detectionNoticeView.visibility = View.GONE
+ }
+
+ // Minimal implementation for use when Flags.SCREENSHOT_METADATA isn't turned on.
+ fun onScreenshotTaken(userHandle: UserHandle) {
+ if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ val workProfileData = workProfileMessageController.onScreenshotTaken(userHandle)
+ if (workProfileData != null) {
+ workProfileFirstRunView.visibility = View.VISIBLE
+ detectionNoticeView.visibility = View.GONE
+
+ workProfileMessageController.populateView(
+ workProfileFirstRunView,
+ workProfileData,
+ this::animateOutMessageContainer
+ )
+ animateInMessageContainer()
+ }
+ }
+ }
+
+ fun onScreenshotTaken(screenshot: ScreenshotData) {
+ if (featureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ val workProfileData =
+ workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
+ var notifiedApps: List<CharSequence> = listOf()
+ if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
+ notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
+ }
+
+ // If work profile first run needs to show, bias towards that, otherwise show screenshot
+ // detection notification if needed.
+ if (workProfileData != null) {
+ workProfileFirstRunView.visibility = View.VISIBLE
+ detectionNoticeView.visibility = View.GONE
+ workProfileMessageController.populateView(
+ workProfileFirstRunView,
+ workProfileData,
+ this::animateOutMessageContainer
+ )
+ animateInMessageContainer()
+ } else if (notifiedApps.isNotEmpty()) {
+ detectionNoticeView.visibility = View.VISIBLE
+ workProfileFirstRunView.visibility = View.GONE
+ screenshotDetectionController.populateView(detectionNoticeView, notifiedApps)
+ animateInMessageContainer()
+ }
+ }
+ }
+
+ private fun animateInMessageContainer() {
+ if (container.visibility == View.VISIBLE) return
+
+ // Need the container to be fully measured before animating in (to know animation offset
+ // destination)
+ container.visibility = View.VISIBLE
+ container.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ container.viewTreeObserver.removeOnPreDrawListener(this)
+ getAnimator(true).start()
+ return false
+ }
+ }
+ )
+ }
+
+ private fun animateOutMessageContainer() {
+ if (animateOut != null) return
+
+ animateOut =
+ getAnimator(false).apply {
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ container.visibility = View.GONE
+ animateOut = null
+ }
+ }
+ )
+ start()
+ }
+ }
+
+ private fun getAnimator(animateIn: Boolean): Animator {
+ val params = container.layoutParams as MarginLayoutParams
+ val offset = container.height + params.topMargin + params.bottomMargin
+ val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f)
+ with(anim) {
+ duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS
+ interpolator = AccelerateDecelerateInterpolator()
+ addUpdateListener { valueAnimator: ValueAnimator ->
+ val interpolation = valueAnimator.animatedValue as Float
+ guideline.setGuidelineEnd((interpolation * offset).toInt())
+ container.alpha = interpolation
+ }
+ }
+ return anim
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 95cc0dc..4db48ac 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -19,33 +19,33 @@
import android.graphics.Insets
import android.util.Log
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
-import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
+import com.android.internal.util.ScreenshotRequest
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
-import java.util.function.Consumer
-import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import java.util.function.Consumer
+import javax.inject.Inject
/**
* Processes a screenshot request sent from {@link ScreenshotHelper}.
*/
@SysUISingleton
class RequestProcessor @Inject constructor(
- private val capture: ImageCapture,
- private val policy: ScreenshotPolicy,
- private val flags: FeatureFlags,
- /** For the Java Async version, to invoke the callback. */
- @Application private val mainScope: CoroutineScope
+ private val capture: ImageCapture,
+ private val policy: ScreenshotPolicy,
+ private val flags: FeatureFlags,
+ /** For the Java Async version, to invoke the callback. */
+ @Application private val mainScope: CoroutineScope
) {
/**
* Inspects the incoming request, returning a potentially modified request depending on policy.
*
* @param request the request to process
*/
+ // TODO: Delete once SCREENSHOT_METADATA flag is launched
suspend fun process(request: ScreenshotRequest): ScreenshotRequest {
var result = request
@@ -58,7 +58,7 @@
// regardless of the managed profile status.
if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
- flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+ flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
) {
val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
@@ -66,17 +66,21 @@
result = if (policy.isManagedProfile(info.user.identifier)) {
val image = capture.captureTask(info.taskId)
- ?: error("Task snapshot returned a null Bitmap!")
+ ?: error("Task snapshot returned a null Bitmap!")
// Provide the task snapshot as the screenshot
- ScreenshotRequest(
- TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source,
- HardwareBitmapBundler.hardwareBitmapToBundle(image),
- info.bounds, Insets.NONE, info.taskId, info.user.identifier, info.component
- )
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source)
+ .setTopComponent(info.component)
+ .setTaskId(info.taskId)
+ .setUserId(info.user.identifier)
+ .setBitmap(image)
+ .setBoundsOnScreen(info.bounds)
+ .setInsets(Insets.NONE)
+ .build()
} else {
// Create a new request of the same type which includes the top component
- ScreenshotRequest(request.type, request.source, info.component)
+ ScreenshotRequest.Builder(request.type, request.source)
+ .setTopComponent(info.component).build()
}
}
@@ -90,12 +94,67 @@
* @param request the request to process
* @param callback the callback to provide the processed request, invoked from the main thread
*/
+ // TODO: Delete once SCREENSHOT_METADATA flag is launched
fun processAsync(request: ScreenshotRequest, callback: Consumer<ScreenshotRequest>) {
mainScope.launch {
val result = process(request)
callback.accept(result)
}
}
+
+ /**
+ * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
+ *
+ * @param screenshot the screenshot to process
+ */
+ suspend fun process(screenshot: ScreenshotData): ScreenshotData {
+ var result = screenshot
+
+ // Apply work profile screenshots policy:
+ //
+ // If the focused app belongs to a work profile, transforms a full screen
+ // (or partial) screenshot request to a task snapshot (provided image) screenshot.
+
+ // Whenever displayContentInfo is fetched, the topComponent is also populated
+ // regardless of the managed profile status.
+
+ if (screenshot.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
+ flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+ ) {
+ val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
+ Log.d(TAG, "findPrimaryContent: $info")
+ result.taskId = info.taskId
+ result.topComponent = info.component
+ result.userHandle = info.user
+
+ if (policy.isManagedProfile(info.user.identifier)) {
+ val image = capture.captureTask(info.taskId)
+ ?: error("Task snapshot returned a null Bitmap!")
+
+ // Provide the task snapshot as the screenshot
+ result.type = TAKE_SCREENSHOT_PROVIDED_IMAGE
+ result.bitmap = image
+ result.screenBounds = info.bounds
+ }
+ }
+
+ return result
+ }
+
+ /**
+ * Note: This is for compatibility with existing Java. Prefer the suspending function when
+ * calling from a Coroutine context.
+ *
+ * @param screenshot the screenshot to process
+ * @param callback the callback to provide the processed screenshot, invoked from the main
+ * thread
+ */
+ fun processAsync(screenshot: ScreenshotData, callback: Consumer<ScreenshotData>) {
+ mainScope.launch {
+ val result = process(screenshot)
+ callback.accept(result)
+ }
+ }
}
private const val TAG = "RequestProcessor"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index b4934cf..bf5fbd2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -20,8 +20,7 @@
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE;
import static com.android.systemui.screenshot.LogConfig.logTag;
-import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION;
-import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS;
+import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;
import android.app.ActivityTaskManager;
import android.app.Notification;
@@ -155,7 +154,8 @@
CompletableFuture<List<Notification.Action>> smartActionsFuture =
mScreenshotSmartActions.getSmartActionsFuture(
- mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS,
+ mScreenshotId, uri, image, mSmartActionsProvider,
+ ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
smartActionsEnabled, user);
List<Notification.Action> smartActions = new ArrayList<>();
if (smartActionsEnabled) {
@@ -166,7 +166,8 @@
smartActions.addAll(buildSmartActions(
mScreenshotSmartActions.getSmartActions(
mScreenshotId, smartActionsFuture, timeoutMs,
- mSmartActionsProvider, REGULAR_SMART_ACTIONS),
+ mSmartActionsProvider,
+ ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
mContext));
}
@@ -476,7 +477,7 @@
CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
mScreenshotSmartActions.getSmartActionsFuture(
mScreenshotId, null, image, mSmartActionsProvider,
- QUICK_SHARE_ACTION,
+ ScreenshotSmartActionType.QUICK_SHARE_ACTION,
true /* smartActionsEnabled */, user);
int timeoutMs = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -485,7 +486,8 @@
List<Notification.Action> quickShareActions =
mScreenshotSmartActions.getSmartActions(
mScreenshotId, quickShareActionsFuture, timeoutMs,
- mSmartActionsProvider, QUICK_SHARE_ACTION);
+ mSmartActionsProvider,
+ ScreenshotSmartActionType.QUICK_SHARE_ACTION);
if (!quickShareActions.isEmpty()) {
mQuickShareData.quickShareAction = quickShareActions.get(0);
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 5716a1d72..72a8e23 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -17,7 +17,6 @@
package com.android.systemui.screenshot;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
@@ -44,6 +43,7 @@
import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks;
import android.app.ICompatCameraControlCallback;
import android.app.Notification;
+import android.app.assist.AssistContent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -101,6 +101,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.util.Assert;
import com.google.common.util.concurrent.ListenableFuture;
@@ -272,6 +273,7 @@
private final ScrollCaptureClient mScrollCaptureClient;
private final PhoneWindow mWindow;
private final DisplayManager mDisplayManager;
+ private final DisplayTracker mDisplayTracker;
private final ScrollCaptureController mScrollCaptureController;
private final LongScreenshotData mLongScreenshotHolder;
private final boolean mIsLowRamDevice;
@@ -280,6 +282,7 @@
private final TimeoutHandler mScreenshotHandler;
private final ActionIntentExecutor mActionExecutor;
private final UserManager mUserManager;
+ private final AssistContentRequester mAssistContentRequester;
private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
if (DEBUG_INPUT) {
@@ -289,6 +292,7 @@
};
private ScreenshotView mScreenshotView;
+ private final MessageContainerController mMessageContainerController;
private Bitmap mScreenBitmap;
private SaveImageInBackgroundTask mSaveInBgTask;
private boolean mScreenshotTakenInPortrait;
@@ -326,7 +330,10 @@
BroadcastSender broadcastSender,
ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
ActionIntentExecutor actionExecutor,
- UserManager userManager
+ UserManager userManager,
+ AssistContentRequester assistContentRequester,
+ MessageContainerController messageContainerController,
+ DisplayTracker displayTracker
) {
mScreenshotSmartActions = screenshotSmartActions;
mNotificationsController = screenshotNotificationsController;
@@ -352,12 +359,15 @@
});
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+ mDisplayTracker = displayTracker;
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mWindowManager = mContext.getSystemService(WindowManager.class);
mFlags = flags;
mActionExecutor = actionExecutor;
mUserManager = userManager;
+ mMessageContainerController = messageContainerController;
+ mAssistContentRequester = assistContentRequester;
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -387,16 +397,147 @@
ClipboardOverlayController.SELF_PERMISSION, null, Context.RECEIVER_NOT_EXPORTED);
}
+ void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
+ RequestCallback requestCallback) {
+ Assert.isMainThread();
+ mCurrentRequestCallback = requestCallback;
+ if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
+ Rect bounds = getFullScreenRect();
+ screenshot.setBitmap(
+ mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(), bounds));
+ screenshot.setScreenBounds(bounds);
+ }
+
+ if (screenshot.getBitmap() == null) {
+ Log.e(TAG, "handleScreenshot: Screenshot bitmap was null");
+ mNotificationsController.notifyScreenshotError(
+ R.string.screenshot_failed_to_capture_text);
+ if (mCurrentRequestCallback != null) {
+ mCurrentRequestCallback.reportError();
+ }
+ return;
+ }
+
+ if (!isUserSetupComplete(Process.myUserHandle())) {
+ Log.w(TAG, "User setup not complete, displaying toast only");
+ // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
+ // and sharing shouldn't be exposed to the user.
+ saveScreenshotAndToast(screenshot.getUserHandle(), finisher);
+ return;
+ }
+
+ mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
+ ClipboardOverlayController.SELF_PERMISSION);
+
+ mScreenshotTakenInPortrait =
+ mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
+
+ String oldPackageName = mPackageName;
+ mPackageName = screenshot.getPackageNameString();
+
+ mScreenBitmap = screenshot.getBitmap();
+ // Optimizations
+ mScreenBitmap.setHasAlpha(false);
+ mScreenBitmap.prepareToDraw();
+
+ prepareViewForNewScreenshot(screenshot, oldPackageName);
+
+ saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
+ this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+
+ // The window is focusable by default
+ setWindowFocusable(true);
+ mScreenshotView.requestFocus();
+
+ enqueueScrollCaptureRequest(screenshot.getUserHandle());
+
+ attachWindow();
+
+ boolean showFlash = true;
+ if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+ if (screenshot.getScreenBounds() != null
+ && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(),
+ screenshot.getScreenBounds())) {
+ showFlash = false;
+ } else {
+ showFlash = true;
+ screenshot.setInsets(Insets.NONE);
+ screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(),
+ screenshot.getBitmap().getHeight()));
+ }
+ }
+
+ prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> {
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mMessageContainerController.onScreenshotTaken(screenshot);
+ }
+ });
+
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
+ mContext.getDrawable(R.drawable.overlay_badge_background),
+ screenshot.getUserHandle()));
+ }
+ mScreenshotView.setScreenshot(screenshot);
+
+ if (screenshot.getTaskId() >= 0) {
+ mAssistContentRequester.requestAssistContent(screenshot.getTaskId(),
+ new AssistContentRequester.Callback() {
+ @Override
+ public void onAssistContentAvailable(AssistContent assistContent) {
+ screenshot.setContextUrl(assistContent.getWebUri());
+ }
+ });
+ }
+
+ if (DEBUG_WINDOW) {
+ Log.d(TAG, "setContentView: " + mScreenshotView);
+ }
+ setContentView(mScreenshotView);
+ // ignore system bar insets for the purpose of window layout
+ mWindow.getDecorView().setOnApplyWindowInsetsListener(
+ (v, insets) -> WindowInsets.CONSUMED);
+ mScreenshotHandler.cancelTimeout(); // restarted after animation
+ }
+
+ void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) {
+ withWindowAttached(() -> {
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+ && mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) {
+ mScreenshotView.announceForAccessibility(mContext.getResources().getString(
+ R.string.screenshot_saving_work_profile_title));
+ } else {
+ mScreenshotView.announceForAccessibility(
+ mContext.getResources().getString(R.string.screenshot_saving_title));
+ }
+ });
+
+ mScreenshotView.reset();
+
+ if (mScreenshotView.isAttachedToWindow()) {
+ // if we didn't already dismiss for another reason
+ if (!mScreenshotView.isDismissing()) {
+ mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0,
+ oldPackageName);
+ }
+ if (DEBUG_WINDOW) {
+ Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
+ + "(dismissing=" + mScreenshotView.isDismissing() + ")");
+ }
+ }
+
+ mScreenshotView.setPackageName(mPackageName);
+
+ mScreenshotView.updateOrientation(
+ mWindowManager.getCurrentWindowMetrics().getWindowInsets());
+ }
+
@MainThread
void takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher,
RequestCallback requestCallback) {
Assert.isMainThread();
mCurrentRequestCallback = requestCallback;
- DisplayMetrics displayMetrics = new DisplayMetrics();
- getDefaultDisplay().getRealMetrics(displayMetrics);
- takeScreenshotInternal(
- topComponent, finisher,
- new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
+ takeScreenshotInternal(topComponent, finisher, getFullScreenRect());
}
@MainThread
@@ -413,10 +554,11 @@
}
boolean showFlash = false;
- if (!aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
+ if (screenshotScreenBounds == null
+ || !aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
showFlash = true;
visibleInsets = Insets.NONE;
- screenshotScreenBounds.set(0, 0, screenshot.getWidth(), screenshot.getHeight());
+ screenshotScreenBounds = new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight());
}
mCurrentRequestCallback = requestCallback;
saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
@@ -493,6 +635,9 @@
// Inflate the screenshot layout
mScreenshotView = (ScreenshotView)
LayoutInflater.from(mContext).inflate(R.layout.screenshot, null);
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mMessageContainerController.setView(mScreenshotView);
+ }
mScreenshotView.addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
@@ -533,6 +678,7 @@
setWindowFocusable(false);
}
}, mActionExecutor, mFlags);
+ mScreenshotView.setDefaultDisplay(mDisplayTracker.getDefaultDisplayId());
mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
@@ -550,6 +696,10 @@
Log.d(TAG, "adding OnComputeInternalInsetsListener");
}
mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView);
+ if (DEBUG_WINDOW) {
+ Log.d(TAG, "setContentView: " + mScreenshotView);
+ }
+ setContentView(mScreenshotView);
}
/**
@@ -562,7 +712,8 @@
// copy the input Rect, since SurfaceControl.screenshot can mutate it
Rect screenRect = new Rect(crop);
- Bitmap screenshot = mImageCapture.captureDisplay(DEFAULT_DISPLAY, crop);
+ Bitmap screenshot = mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(),
+ crop);
if (screenshot == null) {
Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null");
@@ -631,7 +782,49 @@
// The window is focusable by default
setWindowFocusable(true);
+ mScreenshotView.requestFocus();
+ enqueueScrollCaptureRequest(owner);
+
+ attachWindow();
+ prepareAnimation(screenRect, showFlash, () -> {
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mMessageContainerController.onScreenshotTaken(owner);
+ }
+ });
+
+ if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon(
+ mContext.getDrawable(R.drawable.overlay_badge_background), owner));
+ }
+ mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
+ if (DEBUG_WINDOW) {
+ Log.d(TAG, "setContentView: " + mScreenshotView);
+ }
+ setContentView(mScreenshotView);
+ // ignore system bar insets for the purpose of window layout
+ mWindow.getDecorView().setOnApplyWindowInsetsListener(
+ (v, insets) -> WindowInsets.CONSUMED);
+ mScreenshotHandler.cancelTimeout(); // restarted after animation
+ }
+
+ private void prepareAnimation(Rect screenRect, boolean showFlash,
+ Runnable onAnimationComplete) {
+ mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ if (DEBUG_WINDOW) {
+ Log.d(TAG, "onPreDraw: startAnimation");
+ }
+ mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
+ startAnimation(screenRect, showFlash, onAnimationComplete);
+ return true;
+ }
+ });
+ }
+
+ private void enqueueScrollCaptureRequest(UserHandle owner) {
// Wait until this window is attached to request because it is
// the reference used to locate the target window (below).
withWindowAttached(() -> {
@@ -669,34 +862,6 @@
}
});
});
-
- attachWindow();
- mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
- new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- if (DEBUG_WINDOW) {
- Log.d(TAG, "onPreDraw: startAnimation");
- }
- mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
- startAnimation(screenRect, showFlash);
- return true;
- }
- });
-
- if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
- mScreenshotView.badgeScreenshot(
- mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
- }
- mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
- if (DEBUG_WINDOW) {
- Log.d(TAG, "setContentView: " + mScreenshotView);
- }
- setContentView(mScreenshotView);
- // ignore system bar insets for the purpose of window layout
- mWindow.getDecorView().setOnApplyWindowInsetsListener(
- (v, insets) -> WindowInsets.CONSUMED);
- mScreenshotHandler.cancelTimeout(); // restarted after animation
}
private void requestScrollCapture(UserHandle owner) {
@@ -709,7 +874,7 @@
mLastScrollCaptureRequest.cancel(true);
}
final ListenableFuture<ScrollCaptureResponse> future =
- mScrollCaptureClient.request(DEFAULT_DISPLAY);
+ mScrollCaptureClient.request(mDisplayTracker.getDefaultDisplayId());
mLastScrollCaptureRequest = future;
mLastScrollCaptureRequest.addListener(() ->
onScrollCaptureResponseReady(future, owner), mMainExecutor);
@@ -740,7 +905,8 @@
mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
DisplayMetrics displayMetrics = new DisplayMetrics();
getDefaultDisplay().getRealMetrics(displayMetrics);
- Bitmap newScreenshot = mImageCapture.captureDisplay(DEFAULT_DISPLAY,
+ Bitmap newScreenshot = mImageCapture.captureDisplay(
+ mDisplayTracker.getDefaultDisplayId(),
new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
@@ -784,9 +950,9 @@
mLongScreenshotHolder.setLongScreenshot(longScreenshot);
mLongScreenshotHolder.setTransitionDestinationCallback(
(transitionDestination, onTransitionEnd) -> {
- mScreenshotView.startLongScreenshotTransition(
- transitionDestination, onTransitionEnd,
- longScreenshot);
+ mScreenshotView.startLongScreenshotTransition(
+ transitionDestination, onTransitionEnd,
+ longScreenshot);
// TODO: Do this via ActionIntentExecutor instead.
mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
@@ -804,7 +970,8 @@
SCREENSHOT_REMOTE_RUNNER, 0, 0);
try {
WindowManagerGlobal.getWindowManagerService()
- .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
+ .overridePendingAppTransitionRemote(runner,
+ mDisplayTracker.getDefaultDisplayId());
} catch (Exception e) {
Log.e(TAG, "Error overriding screenshot app transition", e);
}
@@ -932,13 +1099,22 @@
/**
* Starts the animation after taking the screenshot
*/
- private void startAnimation(Rect screenRect, boolean showFlash) {
+ private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) {
if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
mScreenshotAnimation.cancel();
}
mScreenshotAnimation =
mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash);
+ if (onAnimationComplete != null) {
+ mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ onAnimationComplete.run();
+ }
+ });
+ }
// Play the shutter sound to notify that we've taken a screenshot
playCameraSound();
@@ -1037,11 +1213,6 @@
private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
mScreenshotView.setChipIntents(imageData);
- if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
- && mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
- // TODO: Read app from configuration
- mScreenshotView.showWorkProfileMessage("Files");
- }
}
/**
@@ -1144,13 +1315,19 @@
}
private Display getDefaultDisplay() {
- return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
+ return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
}
private boolean allowLongScreenshots() {
return !mIsLowRamDevice;
}
+ private Rect getFullScreenRect() {
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ getDefaultDisplay().getRealMetrics(displayMetrics);
+ return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
+ }
+
/** Does the aspect ratio of the bitmap with insets removed match the bounds. */
private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets,
Rect screenBounds) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
new file mode 100644
index 0000000..e9be88a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -0,0 +1,52 @@
+package com.android.systemui.screenshot
+
+import android.content.ComponentName
+import android.graphics.Bitmap
+import android.graphics.Insets
+import android.graphics.Rect
+import android.net.Uri
+import android.os.UserHandle
+import android.view.WindowManager.ScreenshotSource
+import android.view.WindowManager.ScreenshotType
+import androidx.annotation.VisibleForTesting
+import com.android.internal.util.ScreenshotRequest
+
+/** ScreenshotData represents the current state of a single screenshot being acquired. */
+data class ScreenshotData(
+ @ScreenshotType var type: Int,
+ @ScreenshotSource var source: Int,
+ /** UserHandle for the owner of the app being screenshotted, if known. */
+ var userHandle: UserHandle?,
+ /** ComponentName of the top-most app in the screenshot. */
+ var topComponent: ComponentName?,
+ var screenBounds: Rect?,
+ var taskId: Int,
+ var insets: Insets,
+ var bitmap: Bitmap?,
+ /** App-provided URL representing the content the user was looking at in the screenshot. */
+ var contextUrl: Uri? = null,
+) {
+ val packageNameString: String
+ get() = if (topComponent == null) "" else topComponent!!.packageName
+
+ companion object {
+ @JvmStatic
+ fun fromRequest(request: ScreenshotRequest): ScreenshotData {
+ return ScreenshotData(
+ request.type,
+ request.source,
+ if (request.userId >= 0) UserHandle.of(request.userId) else null,
+ request.topComponent,
+ request.boundsInScreen,
+ request.taskId,
+ request.insets,
+ request.bitmap,
+ )
+ }
+
+ @VisibleForTesting
+ fun forTesting(): ScreenshotData {
+ return ScreenshotData(0, 0, null, null, null, 0, Insets.NONE, null)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
new file mode 100644
index 0000000..70ea2b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotDetectionController.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.screenshot
+
+import android.content.pm.PackageManager
+import android.view.IWindowManager
+import android.view.ViewGroup
+import android.widget.TextView
+import com.android.systemui.R
+import javax.inject.Inject
+
+class ScreenshotDetectionController
+@Inject
+constructor(
+ private val windowManager: IWindowManager,
+ private val packageManager: PackageManager,
+) {
+ /**
+ * Notify potentially listening apps of the screenshot. Return a list of the names of the apps
+ * notified.
+ */
+ fun maybeNotifyOfScreenshot(data: ScreenshotData): List<CharSequence> {
+ // TODO: actually ask the window manager once API is available.
+ return listOf()
+ }
+
+ fun populateView(view: ViewGroup, appNames: List<CharSequence>) {
+ assert(appNames.isNotEmpty())
+
+ val textView: TextView = view.requireViewById(R.id.screenshot_detection_notice_text)
+ if (appNames.size == 1) {
+ textView.text =
+ view.resources.getString(R.string.screenshot_detected_template, appNames[0])
+ } else {
+ textView.text =
+ view.resources.getString(
+ R.string.screenshot_detected_multiple_template,
+ appNames[0]
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index 3a35286..21a7310 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -32,12 +32,12 @@
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
-import android.view.Display.DEFAULT_DISPLAY
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.infra.ServiceConnector
import com.android.systemui.SystemUIService
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.settings.DisplayTracker
import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
import java.util.Arrays
import javax.inject.Inject
@@ -52,6 +52,7 @@
private val userMgr: UserManager,
private val atmService: IActivityTaskManager,
@Background val bgDispatcher: CoroutineDispatcher,
+ private val displayTracker: DisplayTracker
) : ScreenshotPolicy {
private val proxyConnector: ServiceConnector<IScreenshotProxy> =
@@ -64,7 +65,7 @@
)
override fun getDefaultDisplayId(): Int {
- return DEFAULT_DISPLAY
+ return displayTracker.defaultDisplayId
}
override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index e8ceb52..afba7ad 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -37,6 +37,7 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -80,7 +81,6 @@
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.TextView;
import androidx.constraintlayout.widget.ConstraintLayout;
@@ -120,7 +120,7 @@
private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
- private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
+ public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
@@ -132,14 +132,13 @@
private final AccessibilityManager mAccessibilityManager;
private final GestureDetector mSwipeDetector;
+ private int mDefaultDisplay = Display.DEFAULT_DISPLAY;
private int mNavMode;
private boolean mOrientationPortrait;
private boolean mDirectionLTR;
private ImageView mScrollingScrim;
private DraggableConstraintLayout mScreenshotStatic;
- private ViewGroup mMessageContainer;
- private TextView mMessageContent;
private ImageView mScreenshotPreview;
private ImageView mScreenshotBadge;
private View mScreenshotPreviewBorder;
@@ -164,6 +163,8 @@
private final ArrayList<OverlayActionChip> mSmartChips = new ArrayList<>();
private PendingInteraction mPendingInteraction;
+ // Should only be set/used if the SCREENSHOT_METADATA flag is set.
+ private ScreenshotData mScreenshotData;
private final InteractionJankMonitor mInteractionJankMonitor;
private long mDefaultTimeoutOfTimeoutHandler;
@@ -289,8 +290,11 @@
mDismissButton.getBoundsOnScreen(tmpRect);
swipeRegion.op(tmpRect, Region.Op.UNION);
- mMessageContainer.findViewById(R.id.message_dismiss_button).getBoundsOnScreen(tmpRect);
- swipeRegion.op(tmpRect, Region.Op.UNION);
+ View messageDismiss = findViewById(R.id.message_dismiss_button);
+ if (messageDismiss != null) {
+ messageDismiss.getBoundsOnScreen(tmpRect);
+ swipeRegion.op(tmpRect, Region.Op.UNION);
+ }
return swipeRegion;
}
@@ -321,7 +325,7 @@
private void startInputListening() {
stopInputListening();
- mInputMonitor = new InputMonitorCompat("Screenshot", Display.DEFAULT_DISPLAY);
+ mInputMonitor = new InputMonitorCompat("Screenshot", mDefaultDisplay);
mInputEventReceiver = mInputMonitor.getInputReceiver(
Looper.getMainLooper(), Choreographer.getInstance(), ev -> {
if (ev instanceof MotionEvent) {
@@ -346,29 +350,11 @@
}
}
- /**
- * Show a notification under the screenshot view indicating that a work profile screenshot has
- * been taken and which app can be used to view it.
- *
- * @param appName The name of the app to use to view screenshots
- */
- void showWorkProfileMessage(String appName) {
- mMessageContent.setText(
- mContext.getString(R.string.screenshot_work_profile_notification, appName));
- mMessageContainer.setVisibility(VISIBLE);
- mMessageContainer.findViewById(R.id.message_dismiss_button).setOnClickListener((v) -> {
- mMessageContainer.setVisibility(View.GONE);
- });
- }
-
@Override // View
protected void onFinishInflate() {
+ super.onFinishInflate();
mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static));
- mMessageContainer =
- requireNonNull(mScreenshotStatic.findViewById(R.id.screenshot_message_container));
- mMessageContent =
- requireNonNull(mMessageContainer.findViewById(R.id.screenshot_message_content));
mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview));
mScreenshotPreviewBorder = requireNonNull(
@@ -458,10 +444,21 @@
mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
}
+ void setScreenshot(ScreenshotData screenshot) {
+ mScreenshotData = screenshot;
+ setScreenshot(screenshot.getBitmap(), screenshot.getInsets());
+ mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, screenshot.getBitmap(),
+ screenshot.getInsets()));
+ }
+
void setPackageName(String packageName) {
mPackageName = packageName;
}
+ void setDefaultDisplay(int displayId) {
+ mDefaultDisplay = displayId;
+ }
+
void updateInsets(WindowInsets insets) {
int orientation = mContext.getResources().getConfiguration().orientation;
mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
@@ -796,9 +793,17 @@
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
prepareSharedTransition();
- mActionExecutor.launchIntentAsync(
- ActionIntentCreator.INSTANCE.createShareIntent(
- imageData.uri, imageData.subject),
+
+ Intent shareIntent;
+ if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && mScreenshotData != null
+ && mScreenshotData.getContextUrl() != null) {
+ shareIntent = ActionIntentCreator.INSTANCE.createShareIntentWithExtraText(
+ imageData.uri, mScreenshotData.getContextUrl().toString());
+ } else {
+ shareIntent = ActionIntentCreator.INSTANCE.createShareIntentWithSubject(
+ imageData.uri, imageData.subject);
+ }
+ mActionExecutor.launchIntentAsync(shareIntent,
imageData.shareTransition.get().bundle,
imageData.owner.getIdentifier(), false);
} else {
@@ -1078,7 +1083,7 @@
mScreenshotBadge.setVisibility(View.GONE);
mScreenshotBadge.setImageDrawable(null);
mPendingSharedTransition = false;
- mActionsContainerBackground.setVisibility(View.GONE);
+ mActionsContainerBackground.setVisibility(View.INVISIBLE);
mActionsContainer.setVisibility(View.GONE);
mDismissButton.setVisibility(View.GONE);
mScrollingScrim.setVisibility(View.GONE);
@@ -1100,6 +1105,7 @@
mQuickShareChip = null;
setAlpha(1);
mScreenshotStatic.setAlpha(1);
+ mScreenshotData = null;
}
private void startSharedTransition(ActionTransition transition) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 2176825..4214c8f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -21,7 +21,6 @@
import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
-import static com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR;
import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
@@ -55,11 +54,12 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.FlagListenable.FlagEvent;
+import com.android.systemui.flags.Flags;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -122,7 +122,6 @@
mContext = context;
mBgExecutor = bgExecutor;
mFeatureFlags = featureFlags;
- mFeatureFlags.addListener(SCREENSHOT_REQUEST_PROCESSOR, FlagEvent::requestNoRestart);
mFeatureFlags.addListener(SCREENSHOT_WORK_PROFILE_POLICY, FlagEvent::requestNoRestart);
mProcessor = processor;
}
@@ -188,8 +187,7 @@
final Consumer<Uri> onSaved = (uri) -> reportUri(replyTo, uri);
RequestCallback callback = new RequestCallbackImpl(replyTo);
- ScreenshotHelper.ScreenshotRequest request =
- (ScreenshotHelper.ScreenshotRequest) msg.obj;
+ ScreenshotRequest request = (ScreenshotRequest) msg.obj;
handleRequest(request, onSaved, callback);
return true;
@@ -197,7 +195,7 @@
@MainThread
@VisibleForTesting
- void handleRequest(ScreenshotHelper.ScreenshotRequest request, Consumer<Uri> onSaved,
+ void handleRequest(ScreenshotRequest request, Consumer<Uri> onSaved,
RequestCallback callback) {
// If the storage for this user is locked, we have no place to store
// the screenshot, so skip taking it instead of showing a misleading
@@ -224,17 +222,36 @@
return;
}
- if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) {
- Log.d(TAG, "handleMessage: Using request processor");
+ if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_METADATA)) {
+ Log.d(TAG, "Processing screenshot data");
+ ScreenshotData screenshotData = ScreenshotData.fromRequest(request);
+ mProcessor.processAsync(screenshotData,
+ (data) -> dispatchToController(data, onSaved, callback));
+ } else {
mProcessor.processAsync(request,
(r) -> dispatchToController(r, onSaved, callback));
+ }
+ }
+
+ private void dispatchToController(ScreenshotData screenshot,
+ Consumer<Uri> uriConsumer, RequestCallback callback) {
+
+ mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
+ screenshot.getPackageNameString());
+
+ if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
+ && screenshot.getBitmap() == null) {
+ Log.e(TAG, "Got null bitmap from screenshot message");
+ mNotificationsController.notifyScreenshotError(
+ R.string.screenshot_failed_to_capture_text);
+ callback.reportError();
return;
}
- dispatchToController(request, onSaved, callback);
+ mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
}
- private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
+ private void dispatchToController(ScreenshotRequest request,
Consumer<Uri> uriConsumer, RequestCallback callback) {
ComponentName topComponent = request.getTopComponent();
@@ -252,8 +269,7 @@
if (DEBUG_SERVICE) {
Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_PROVIDED_IMAGE");
}
- Bitmap screenshot = ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap(
- request.getBitmapBundle());
+ Bitmap screenshot = request.getBitmap();
Rect screenBounds = request.getBoundsInScreen();
Insets insets = request.getInsets();
int taskId = request.getTaskId();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
new file mode 100644
index 0000000..1b728b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/WorkProfileMessageController.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.systemui.R
+import javax.inject.Inject
+
+/**
+ * Handles work profile first run, determining whether a first run UI should be shown and populating
+ * that UI if needed.
+ */
+class WorkProfileMessageController
+@Inject
+constructor(
+ private val context: Context,
+ private val userManager: UserManager,
+ private val packageManager: PackageManager,
+) {
+
+ /**
+ * @return a populated WorkProfileFirstRunData object if a work profile first run message should
+ * be shown
+ */
+ fun onScreenshotTaken(userHandle: UserHandle?): WorkProfileFirstRunData? {
+ if (userHandle == null) return null
+
+ if (userManager.isManagedProfile(userHandle.identifier) && !messageAlreadyDismissed()) {
+ var badgedIcon: Drawable? = null
+ var label: CharSequence? = null
+ val fileManager = fileManagerComponentName()
+ try {
+ val info =
+ packageManager.getActivityInfo(
+ fileManager,
+ PackageManager.ComponentInfoFlags.of(0)
+ )
+ val icon = packageManager.getActivityIcon(fileManager)
+ badgedIcon = packageManager.getUserBadgedIcon(icon, userHandle)
+ label = info.loadLabel(packageManager)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Component $fileManager not found")
+ }
+
+ // If label wasn't loaded, use a default
+ return WorkProfileFirstRunData(label ?: defaultFileAppName(), badgedIcon)
+ }
+ return null
+ }
+
+ /**
+ * Use the provided WorkProfileFirstRunData to populate the work profile first run UI in the
+ * given view.
+ */
+ fun populateView(view: ViewGroup, data: WorkProfileFirstRunData, animateOut: () -> Unit) {
+ if (data.icon != null) {
+ // Replace the default icon if one is provided.
+ val imageView: ImageView = view.requireViewById<ImageView>(R.id.screenshot_message_icon)
+ imageView.setImageDrawable(data.icon)
+ }
+ val messageContent = view.requireViewById<TextView>(R.id.screenshot_message_content)
+ messageContent.text =
+ view.context.getString(R.string.screenshot_work_profile_notification, data.appName)
+ view.requireViewById<View>(R.id.message_dismiss_button).setOnClickListener {
+ animateOut()
+ onMessageDismissed()
+ }
+ }
+
+ private fun messageAlreadyDismissed(): Boolean {
+ return sharedPreference().getBoolean(PREFERENCE_KEY, false)
+ }
+
+ private fun onMessageDismissed() {
+ val editor = sharedPreference().edit()
+ editor.putBoolean(PREFERENCE_KEY, true)
+ editor.apply()
+ }
+
+ private fun sharedPreference() =
+ context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+
+ private fun fileManagerComponentName() =
+ ComponentName.unflattenFromString(
+ context.getString(R.string.config_sceenshotWorkProfileFilesApp)
+ )
+
+ private fun defaultFileAppName() = context.getString(R.string.screenshot_default_files_app_name)
+
+ data class WorkProfileFirstRunData constructor(val appName: CharSequence, val icon: Drawable?)
+
+ companion object {
+ const val TAG = "WorkProfileMessageCtrl"
+ const val SHARED_PREFERENCES_NAME = "com.android.systemui.screenshot"
+ const val PREFERENCE_KEY = "work_profile_first_run"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt
new file mode 100644
index 0000000..bb7f721
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.view.Display
+import java.util.concurrent.Executor
+
+/**
+ * Display tracker for SystemUI.
+ *
+ * This tracker provides async access to display information, as well as callbacks for display
+ * changes.
+ */
+interface DisplayTracker {
+
+ /** The id for the default display for the current SystemUI instance. */
+ val defaultDisplayId: Int
+
+ /** All displays that should be associated with the current SystemUI instance. */
+ val allDisplays: Array<Display>
+
+ /**
+ * Add a [Callback] to be notified of display changes, including additions, removals, and
+ * configuration changes, on a particular [Executor].
+ */
+ fun addDisplayChangeCallback(callback: Callback, executor: Executor)
+
+ /**
+ * Add a [Callback] to be notified of display brightness changes, on a particular [Executor].
+ * This callback will trigger Callback#onDisplayChanged for a display brightness change.
+ */
+ fun addBrightnessChangeCallback(callback: Callback, executor: Executor)
+
+ /** Remove a [Callback] previously added. */
+ fun removeCallback(callback: Callback)
+
+ /** Ćallback for notifying of changes. */
+ interface Callback {
+
+ /** Notifies that a display has been added. */
+ @JvmDefault fun onDisplayAdded(displayId: Int) {}
+
+ /** Notifies that a display has been removed. */
+ @JvmDefault fun onDisplayRemoved(displayId: Int) {}
+
+ /** Notifies a display has been changed */
+ @JvmDefault fun onDisplayChanged(displayId: Int) {}
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
new file mode 100644
index 0000000..5169f88
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTrackerImpl.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS
+import android.os.Handler
+import android.view.Display
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.Assert
+import java.lang.ref.WeakReference
+import java.util.concurrent.Executor
+
+class DisplayTrackerImpl
+internal constructor(
+ val displayManager: DisplayManager,
+ @Background val backgroundHandler: Handler
+) : DisplayTracker {
+ override val defaultDisplayId: Int = Display.DEFAULT_DISPLAY
+ override val allDisplays: Array<Display>
+ get() = displayManager.displays
+
+ @GuardedBy("displayCallbacks")
+ private val displayCallbacks: MutableList<DisplayTrackerDataItem> = ArrayList()
+ @GuardedBy("brightnessCallbacks")
+ private val brightnessCallbacks: MutableList<DisplayTrackerDataItem> = ArrayList()
+
+ @VisibleForTesting
+ val displayChangedListener: DisplayManager.DisplayListener =
+ object : DisplayManager.DisplayListener {
+ override fun onDisplayAdded(displayId: Int) {
+ val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+ onDisplayAdded(displayId, list)
+ }
+
+ override fun onDisplayRemoved(displayId: Int) {
+ val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+ onDisplayRemoved(displayId, list)
+ }
+
+ override fun onDisplayChanged(displayId: Int) {
+ val list = synchronized(displayCallbacks) { displayCallbacks.toList() }
+ onDisplayChanged(displayId, list)
+ }
+ }
+
+ @VisibleForTesting
+ val displayBrightnessChangedListener: DisplayManager.DisplayListener =
+ object : DisplayManager.DisplayListener {
+ override fun onDisplayAdded(displayId: Int) {}
+
+ override fun onDisplayRemoved(displayId: Int) {}
+
+ override fun onDisplayChanged(displayId: Int) {
+ val list = synchronized(brightnessCallbacks) { brightnessCallbacks.toList() }
+ onDisplayChanged(displayId, list)
+ }
+ }
+
+ override fun addDisplayChangeCallback(callback: DisplayTracker.Callback, executor: Executor) {
+ synchronized(displayCallbacks) {
+ if (displayCallbacks.isEmpty()) {
+ displayManager.registerDisplayListener(displayChangedListener, backgroundHandler)
+ }
+ displayCallbacks.add(DisplayTrackerDataItem(WeakReference(callback), executor))
+ }
+ }
+
+ override fun addBrightnessChangeCallback(
+ callback: DisplayTracker.Callback,
+ executor: Executor
+ ) {
+ synchronized(brightnessCallbacks) {
+ if (brightnessCallbacks.isEmpty()) {
+ displayManager.registerDisplayListener(
+ displayBrightnessChangedListener,
+ backgroundHandler,
+ EVENT_FLAG_DISPLAY_BRIGHTNESS
+ )
+ }
+ brightnessCallbacks.add(DisplayTrackerDataItem(WeakReference(callback), executor))
+ }
+ }
+
+ override fun removeCallback(callback: DisplayTracker.Callback) {
+ synchronized(displayCallbacks) {
+ val changed = displayCallbacks.removeIf { it.sameOrEmpty(callback) }
+ if (changed && displayCallbacks.isEmpty()) {
+ displayManager.unregisterDisplayListener(displayChangedListener)
+ }
+ }
+
+ synchronized(brightnessCallbacks) {
+ val changed = brightnessCallbacks.removeIf { it.sameOrEmpty(callback) }
+ if (changed && brightnessCallbacks.isEmpty()) {
+ displayManager.unregisterDisplayListener(displayBrightnessChangedListener)
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun onDisplayAdded(displayId: Int, list: List<DisplayTrackerDataItem>) {
+ Assert.isNotMainThread()
+
+ notifySubscribers({ onDisplayAdded(displayId) }, list)
+ }
+
+ @WorkerThread
+ private fun onDisplayRemoved(displayId: Int, list: List<DisplayTrackerDataItem>) {
+ Assert.isNotMainThread()
+
+ notifySubscribers({ onDisplayRemoved(displayId) }, list)
+ }
+
+ @WorkerThread
+ private fun onDisplayChanged(displayId: Int, list: List<DisplayTrackerDataItem>) {
+ Assert.isNotMainThread()
+
+ notifySubscribers({ onDisplayChanged(displayId) }, list)
+ }
+
+ private inline fun notifySubscribers(
+ crossinline action: DisplayTracker.Callback.() -> Unit,
+ list: List<DisplayTrackerDataItem>
+ ) {
+ list.forEach {
+ if (it.callback.get() != null) {
+ it.executor.execute { it.callback.get()?.action() }
+ }
+ }
+ }
+
+ private data class DisplayTrackerDataItem(
+ val callback: WeakReference<DisplayTracker.Callback>,
+ val executor: Executor
+ ) {
+ fun sameOrEmpty(other: DisplayTracker.Callback): Boolean {
+ return callback.get()?.equals(other) ?: true
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index 1558ac5..287e810 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -62,12 +62,24 @@
fun removeCallback(callback: Callback)
/**
- * Ćallback for notifying of changes.
+ * Callback for notifying of changes.
*/
interface Callback {
/**
+ * Notifies that the current user is being changed.
+ * Override this method to run things while the screen is frozen for the user switch.
+ * Please use {@link #onUserChanged} if the task doesn't need to push the unfreezing of the
+ * screen further. Please be aware that code executed in this callback will lengthen the
+ * user switch duration.
+ */
+ @JvmDefault
+ fun onUserChanging(newUser: Int, userContext: Context) {}
+
+ /**
* Notifies that the current user has changed.
+ * Override this method to run things after the screen is unfrozen for the user switch.
+ * Please see {@link #onUserChanging} if you need to hide jank.
*/
@JvmDefault
fun onUserChanged(newUser: Int, userContext: Context) {}
@@ -78,4 +90,4 @@
@JvmDefault
fun onProfilesChanged(profiles: List<@JvmSuppressWildcards UserInfo>) {}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 28da38b..9f551c6 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -16,6 +16,8 @@
package com.android.systemui.settings
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
@@ -23,6 +25,7 @@
import android.content.IntentFilter
import android.content.pm.UserInfo
import android.os.Handler
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.util.Log
@@ -34,6 +37,7 @@
import java.io.PrintWriter
import java.lang.IllegalStateException
import java.lang.ref.WeakReference
+import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@@ -56,6 +60,7 @@
class UserTrackerImpl internal constructor(
private val context: Context,
private val userManager: UserManager,
+ private val iActivityManager: IActivityManager,
private val dumpManager: DumpManager,
private val backgroundHandler: Handler
) : UserTracker, Dumpable, BroadcastReceiver() {
@@ -107,28 +112,27 @@
setUserIdInternal(startingUser)
val filter = IntentFilter().apply {
- addAction(Intent.ACTION_USER_SWITCHED)
addAction(Intent.ACTION_USER_INFO_CHANGED)
// These get called when a managed profile goes in or out of quiet mode.
addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
-
+ addAction(Intent.ACTION_MANAGED_PROFILE_ADDED)
addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)
addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)
}
context.registerReceiverForAllUsers(this, filter, null /* permission */, backgroundHandler)
+ registerUserSwitchObserver()
+
dumpManager.registerDumpable(TAG, this)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
- Intent.ACTION_USER_SWITCHED -> {
- handleSwitchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL))
- }
Intent.ACTION_USER_INFO_CHANGED,
Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
+ Intent.ACTION_MANAGED_PROFILE_ADDED,
Intent.ACTION_MANAGED_PROFILE_REMOVED,
Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> {
handleProfilesChanged()
@@ -156,22 +160,43 @@
return ctx to profiles
}
+ private fun registerUserSwitchObserver() {
+ iActivityManager.registerUserSwitchObserver(object : UserSwitchObserver() {
+ override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
+ backgroundHandler.run {
+ handleUserSwitching(newUserId)
+ reply?.sendResult(null)
+ }
+ }
+
+ override fun onUserSwitchComplete(newUserId: Int) {
+ backgroundHandler.run {
+ handleUserSwitchComplete(newUserId)
+ }
+ }
+ }, TAG)
+ }
+
@WorkerThread
- private fun handleSwitchUser(newUser: Int) {
+ private fun handleUserSwitching(newUserId: Int) {
Assert.isNotMainThread()
- if (newUser == UserHandle.USER_NULL) {
- Log.w(TAG, "handleSwitchUser - Couldn't get new id from intent")
- return
- }
+ Log.i(TAG, "Switching to user $newUserId")
- if (newUser == userId) return
- Log.i(TAG, "Switching to user $newUser")
-
- val (ctx, profiles) = setUserIdInternal(newUser)
-
+ setUserIdInternal(newUserId)
notifySubscribers {
- onUserChanged(newUser, ctx)
- onProfilesChanged(profiles)
+ onUserChanging(newUserId, userContext)
+ }.await()
+ }
+
+ @WorkerThread
+ private fun handleUserSwitchComplete(newUserId: Int) {
+ Assert.isNotMainThread()
+ Log.i(TAG, "Switched to user $newUserId")
+
+ setUserIdInternal(newUserId)
+ notifySubscribers {
+ onUserChanged(newUserId, userContext)
+ onProfilesChanged(userProfiles)
}
}
@@ -200,17 +225,25 @@
}
}
- private inline fun notifySubscribers(crossinline action: UserTracker.Callback.() -> Unit) {
+ private inline fun notifySubscribers(
+ crossinline action: UserTracker.Callback.() -> Unit
+ ): CountDownLatch {
val list = synchronized(callbacks) {
callbacks.toList()
}
+ val latch = CountDownLatch(list.size)
+
list.forEach {
if (it.callback.get() != null) {
it.executor.execute {
it.callback.get()?.action()
+ latch.countDown()
}
+ } else {
+ latch.countDown()
}
}
+ return latch
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -257,4 +290,4 @@
fun sameOrEmpty(other: UserTracker.Callback): Boolean {
return callback.get()?.equals(other) ?: true
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 5880003..8089d01 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -27,10 +27,10 @@
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManager.DisplayListener;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
@@ -49,6 +49,7 @@
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -78,19 +79,14 @@
private final ToggleSlider mControl;
private final DisplayManager mDisplayManager;
private final UserTracker mUserTracker;
+ private final DisplayTracker mDisplayTracker;
private final IVrManager mVrManager;
private final Executor mMainExecutor;
private final Handler mBackgroundHandler;
private final BrightnessObserver mBrightnessObserver;
- private final DisplayListener mDisplayListener = new DisplayListener() {
- @Override
- public void onDisplayAdded(int displayId) {}
-
- @Override
- public void onDisplayRemoved(int displayId) {}
-
+ private final DisplayTracker.Callback mBrightnessListener = new DisplayTracker.Callback() {
@Override
public void onDisplayChanged(int displayId) {
mBackgroundHandler.post(mUpdateSliderRunnable);
@@ -143,14 +139,14 @@
cr.registerContentObserver(
BRIGHTNESS_FOR_VR_FLOAT_URI,
false, this, UserHandle.USER_ALL);
- mDisplayManager.registerDisplayListener(mDisplayListener, mHandler,
- DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
+ mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener,
+ new HandlerExecutor(mHandler));
}
public void stopObserving() {
final ContentResolver cr = mContext.getContentResolver();
cr.unregisterContentObserver(this);
- mDisplayManager.unregisterDisplayListener(mDisplayListener);
+ mDisplayTracker.removeCallback(mBrightnessListener);
}
}
@@ -218,7 +214,7 @@
automatic = Settings.System.getIntForUser(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL,
- UserHandle.USER_CURRENT);
+ mUserTracker.getUserId());
mAutomatic = automatic != Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL;
}
};
@@ -292,6 +288,7 @@
Context context,
ToggleSlider control,
UserTracker userTracker,
+ DisplayTracker displayTracker,
@Main Executor mainExecutor,
@Background Handler bgHandler) {
mContext = context;
@@ -300,6 +297,7 @@
mMainExecutor = mainExecutor;
mBackgroundHandler = bgHandler;
mUserTracker = userTracker;
+ mDisplayTracker = displayTracker;
mBrightnessObserver = new BrightnessObserver(mHandler);
mDisplayId = mContext.getDisplayId();
@@ -450,6 +448,7 @@
public static class Factory {
private final Context mContext;
private final UserTracker mUserTracker;
+ private final DisplayTracker mDisplayTracker;
private final Executor mMainExecutor;
private final Handler mBackgroundHandler;
@@ -457,10 +456,12 @@
public Factory(
Context context,
UserTracker userTracker,
+ DisplayTracker displayTracker,
@Main Executor mainExecutor,
@Background Handler bgHandler) {
mContext = context;
mUserTracker = userTracker;
+ mDisplayTracker = displayTracker;
mMainExecutor = mainExecutor;
mBackgroundHandler = bgHandler;
}
@@ -471,6 +472,7 @@
mContext,
toggleSlider,
mUserTracker,
+ mDisplayTracker,
mMainExecutor,
mBackgroundHandler);
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index e208be9..8879501 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -36,6 +36,7 @@
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import java.util.List;
@@ -49,16 +50,19 @@
private BrightnessController mBrightnessController;
private final BrightnessSliderController.Factory mToggleSliderFactory;
private final UserTracker mUserTracker;
+ private final DisplayTracker mDisplayTracker;
private final Executor mMainExecutor;
private final Handler mBackgroundHandler;
@Inject
public BrightnessDialog(
UserTracker userTracker,
+ DisplayTracker displayTracker,
BrightnessSliderController.Factory factory,
@Main Executor mainExecutor,
@Background Handler bgHandler) {
mUserTracker = userTracker;
+ mDisplayTracker = displayTracker;
mToggleSliderFactory = factory;
mMainExecutor = mainExecutor;
mBackgroundHandler = bgHandler;
@@ -106,7 +110,7 @@
frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT);
mBrightnessController = new BrightnessController(
- this, controller, mUserTracker, mMainExecutor, mBackgroundHandler);
+ this, controller, mUserTracker, mDisplayTracker, mMainExecutor, mBackgroundHandler);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
index 2f62e44..e9a1dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/dagger/MultiUserUtilsModule.java
@@ -17,7 +17,9 @@
package com.android.systemui.settings.dagger;
import android.app.ActivityManager;
+import android.app.IActivityManager;
import android.content.Context;
+import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.UserManager;
@@ -25,6 +27,8 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.settings.DisplayTrackerImpl;
import com.android.systemui.settings.UserContentResolverProvider;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserFileManager;
@@ -57,15 +61,26 @@
static UserTracker provideUserTracker(
Context context,
UserManager userManager,
+ IActivityManager iActivityManager,
DumpManager dumpManager,
@Background Handler handler
) {
int startingUser = ActivityManager.getCurrentUser();
- UserTrackerImpl tracker = new UserTrackerImpl(context, userManager, dumpManager, handler);
+ UserTrackerImpl tracker = new UserTrackerImpl(context, userManager, iActivityManager,
+ dumpManager, handler);
tracker.initialize(startingUser);
return tracker;
}
+ @SysUISingleton
+ @Provides
+ static DisplayTracker provideDisplayTracker(
+ DisplayManager displayManager,
+ @Background Handler handler
+ ) {
+ return new DisplayTrackerImpl(displayManager, handler);
+ }
+
@Binds
@IntoMap
@ClassKey(UserFileManagerImpl.class)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 5011227..b3d31f2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -69,7 +69,8 @@
}
return ConstraintsChanges(
qqsConstraintsChanges = change,
- qsConstraintsChanges = change
+ qsConstraintsChanges = change,
+ largeScreenConstraintsChanges = change,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index 7fc0a5f..197232e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
+import android.animation.Animator
import android.annotation.IdRes
import android.app.StatusBarManager
import android.content.res.Configuration
@@ -45,7 +46,6 @@
import com.android.systemui.qs.carrier.QSCarrierGroup
import com.android.systemui.qs.carrier.QSCarrierGroupController
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.HEADER_TRANSITION_ID
-import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
@@ -113,7 +113,7 @@
QQS_HEADER_CONSTRAINT -> "QQS Header"
QS_HEADER_CONSTRAINT -> "QS Header"
LARGE_SCREEN_HEADER_CONSTRAINT -> "Large Screen Header"
- else -> "Unknown state"
+ else -> "Unknown state $this"
}
}
@@ -175,9 +175,14 @@
*/
var shadeExpandedFraction = -1f
set(value) {
- if (visible && field != value) {
+ if (field != value) {
+ val oldAlpha = header.alpha
header.alpha = ShadeInterpolation.getContentAlpha(value)
field = value
+ if ((oldAlpha == 0f && header.alpha > 0f) ||
+ (oldAlpha > 0f && header.alpha == 0f)) {
+ updateVisibility()
+ }
}
}
@@ -295,21 +300,23 @@
override fun onViewAttached() {
privacyIconsController.chipVisibilityListener = chipVisibilityListener
+ updateVisibility()
+ updateTransition()
+
if (header is MotionLayout) {
header.setOnApplyWindowInsetsListener(insetListener)
clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
v.pivotX = newPivot
v.pivotY = v.height.toFloat() / 2
+
+ qsCarrierGroup.setPaddingRelative((v.width * v.scaleX).toInt(), 0, 0, 0)
}
}
dumpManager.registerDumpable(this)
configurationController.addCallback(configurationControllerListener)
demoModeController.addCallback(demoModeReceiver)
-
- updateVisibility()
- updateTransition()
}
override fun onViewDetached() {
@@ -331,9 +338,31 @@
.setDuration(duration)
.alpha(if (show) 0f else 1f)
.setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN)
+ .setUpdateListener {
+ updateVisibility()
+ }
+ .setListener(endAnimationListener)
.start()
}
+ private val endAnimationListener = object : Animator.AnimatorListener {
+ override fun onAnimationCancel(animation: Animator?) {
+ clearListeners()
+ }
+
+ override fun onAnimationEnd(animation: Animator?) {
+ clearListeners()
+ }
+
+ override fun onAnimationRepeat(animation: Animator?) {}
+
+ override fun onAnimationStart(animation: Animator?) {}
+
+ private fun clearListeners() {
+ header.animate().setListener(null).setUpdateListener(null)
+ }
+ }
+
private fun loadConstraints() {
if (header is MotionLayout) {
// Use resources.getXml instead of passing the resource id due to bug b/205018300
@@ -414,7 +443,7 @@
private fun updateVisibility() {
val visibility = if (!largeScreenActive && !combinedHeaders || qsDisabled) {
View.GONE
- } else if (qsVisible) {
+ } else if (qsVisible && header.alpha > 0f) {
View.VISIBLE
} else {
View.INVISIBLE
@@ -432,15 +461,14 @@
header as MotionLayout
if (largeScreenActive) {
logInstantEvent("Large screen constraints set")
- header.setTransition(HEADER_TRANSITION_ID)
- header.transitionToStart()
+ header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
} else {
logInstantEvent("Small screen constraints set")
header.setTransition(HEADER_TRANSITION_ID)
- header.transitionToStart()
- updatePosition()
- updateScrollY()
}
+ header.jumpToState(header.startState)
+ updatePosition()
+ updateScrollY()
}
private fun updatePosition() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 8f512d0..296c631 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -138,12 +138,20 @@
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -200,7 +208,6 @@
import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm;
import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
@@ -239,6 +246,7 @@
import javax.inject.Inject;
import javax.inject.Provider;
+import kotlin.Unit;
import kotlinx.coroutines.CoroutineDispatcher;
@CentralSurfacesComponent.CentralSurfacesScope
@@ -360,6 +368,7 @@
private final FragmentListener mQsFragmentListener = new QsFragmentListener();
private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate();
private final NotificationGutsManager mGutsManager;
+ private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private long mDownTime;
private boolean mTouchSlopExceededBeforeDown;
@@ -445,6 +454,7 @@
private float mDownY;
private int mDisplayTopInset = 0; // in pixels
private int mDisplayRightInset = 0; // in pixels
+ private int mDisplayLeftInset = 0; // in pixels
private int mLargeScreenShadeHeaderHeight;
private int mSplitShadeNotificationsScrimMarginBottom;
@@ -687,12 +697,19 @@
private boolean mExpandLatencyTracking;
private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+ private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
+ private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
+ private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private final KeyguardInteractor mKeyguardInteractor;
private CoroutineDispatcher mMainDispatcher;
- private boolean mIsToLockscreenTransitionRunning = false;
+ private boolean mIsOcclusionTransitionRunning = false;
private int mDreamingToLockscreenTransitionTranslationY;
private int mOccludedToLockscreenTransitionTranslationY;
+ private int mLockscreenToDreamingTransitionTranslationY;
+ private int mGoneToDreamingTransitionTranslationY;
+ private int mLockscreenToOccludedTransitionTranslationY;
private boolean mUnocclusionTransitionFlagEnabled = false;
private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
@@ -704,20 +721,38 @@
updatePanelExpansionAndVisibility();
};
private final Runnable mMaybeHideExpandedRunnable = () -> {
- if (getExpansionFraction() == 0.0f) {
+ if (getExpandedFraction() == 0.0f) {
postToView(mHideExpandedRunnable);
}
};
private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
(TransitionStep step) -> {
- mIsToLockscreenTransitionRunning =
+ mIsOcclusionTransitionRunning =
step.getTransitionState() == TransitionState.RUNNING;
};
private final Consumer<TransitionStep> mOccludedToLockscreenTransition =
(TransitionStep step) -> {
- mIsToLockscreenTransitionRunning =
+ mIsOcclusionTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
+ private final Consumer<TransitionStep> mLockscreenToDreamingTransition =
+ (TransitionStep step) -> {
+ mIsOcclusionTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
+ private final Consumer<TransitionStep> mGoneToDreamingTransition =
+ (TransitionStep step) -> {
+ mIsOcclusionTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
+ private final Consumer<TransitionStep> mLockscreenToOccludedTransition =
+ (TransitionStep step) -> {
+ mIsOcclusionTransitionRunning =
step.getTransitionState() == TransitionState.RUNNING;
};
@@ -789,11 +824,17 @@
SystemClock systemClock,
KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
+ AlternateBouncerInteractor alternateBouncerInteractor,
DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
+ LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel,
+ GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel,
+ LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ KeyguardLongPressViewModel keyguardLongPressViewModel,
+ KeyguardInteractor keyguardInteractor) {
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
public void onKeyguardFadingAwayChanged() {
@@ -810,7 +851,11 @@
mGutsManager = gutsManager;
mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
+ mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel;
+ mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel;
+ mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+ mKeyguardInteractor = keyguardInteractor;
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
@@ -963,6 +1008,14 @@
updateUserSwitcherFlags();
mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
+ KeyguardLongPressViewBinder.bind(
+ mView.requireViewById(R.id.keyguard_long_press),
+ keyguardLongPressViewModel,
+ () -> {
+ onEmptySpaceClick();
+ return Unit.INSTANCE;
+ },
+ mFalsingManager);
onFinishInflate();
keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -980,6 +1033,7 @@
unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay);
}
});
+ mAlternateBouncerInteractor = alternateBouncerInteractor;
dumpManager.registerDumpable(this);
}
@@ -1117,22 +1171,55 @@
collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
mDreamingToLockscreenTransition, mMainDispatcher);
collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
- toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ setTransitionAlpha(mNotificationStackScrollLayoutController),
mMainDispatcher);
collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
mDreamingToLockscreenTransitionTranslationY),
- toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ setTransitionY(mNotificationStackScrollLayoutController),
mMainDispatcher);
// Occluded->Lockscreen
collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
mOccludedToLockscreenTransition, mMainDispatcher);
collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
- toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ setTransitionAlpha(mNotificationStackScrollLayoutController),
mMainDispatcher);
collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(
mOccludedToLockscreenTransitionTranslationY),
- toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ setTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
+ // Lockscreen->Dreaming
+ collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(),
+ mLockscreenToDreamingTransition, mMainDispatcher);
+ collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(),
+ setTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mLockscreenToDreamingTransitionViewModel.lockscreenTranslationY(
+ mLockscreenToDreamingTransitionTranslationY),
+ setTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
+ // Gone->Dreaming
+ collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
+ mGoneToDreamingTransition, mMainDispatcher);
+ collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
+ setTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mGoneToDreamingTransitionViewModel.lockscreenTranslationY(
+ mGoneToDreamingTransitionTranslationY),
+ setTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
+ // Lockscreen->Occluded
+ collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(),
+ mLockscreenToOccludedTransition, mMainDispatcher);
+ collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(),
+ setTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mLockscreenToOccludedTransitionViewModel.lockscreenTranslationY(
+ mLockscreenToOccludedTransitionTranslationY),
+ setTransitionY(mNotificationStackScrollLayoutController),
mMainDispatcher);
}
}
@@ -1173,6 +1260,12 @@
R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
+ mLockscreenToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.lockscreen_to_dreaming_transition_lockscreen_translation_y);
+ mGoneToDreamingTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.gone_to_dreaming_transition_lockscreen_translation_y);
+ mLockscreenToOccludedTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y);
}
private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1836,7 +1929,7 @@
}
private void updateClock() {
- if (mIsToLockscreenTransitionRunning) {
+ if (mIsOcclusionTransitionRunning) {
return;
}
float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
@@ -2020,7 +2113,17 @@
}
}
- public void expandWithoutQs() {
+ /**
+ * Expand shade so that notifications are visible.
+ * Non-split shade: just expanding shade or collapsing QS when they're expanded.
+ * Split shade: only expanding shade, notifications are always visible
+ *
+ * Called when `adb shell cmd statusbar expand-notifications` is executed.
+ */
+ public void expandShadeToNotifications() {
+ if (mSplitShadeEnabled && (isShadeFullyOpen() || isExpanding())) {
+ return;
+ }
if (isQsExpanded()) {
flingSettings(0 /* velocity */, FLING_COLLAPSE);
} else {
@@ -2070,6 +2173,7 @@
}
ValueAnimator animator = createHeightAnimator(target, overshootAmount);
if (expand) {
+ maybeVibrateOnOpening(true /* openingWithTouch */);
if (expandBecauseOfFalsing && vel < 0) {
vel = 0;
}
@@ -2080,6 +2184,7 @@
animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION);
}
} else {
+ mHasVibratedOnOpen = false;
if (shouldUseDismissingAnimation()) {
if (vel == 0) {
animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
@@ -2288,7 +2393,7 @@
// When false, down but not synthesized motion event.
mLastEventSynthesizedDown = mExpectingSynthesizedDown;
mLastDownEvents.insert(
- mSystemClock.currentTimeMillis(),
+ event.getEventTime(),
mDownX,
mDownY,
mQsTouchAboveFalsingThreshold,
@@ -2421,7 +2526,7 @@
mInitialTouchY = event.getY();
mInitialTouchX = event.getX();
}
- if (!isFullyCollapsed()) {
+ if (!isFullyCollapsed() && !isShadeOrQsHeightAnimationRunning()) {
handleQsDown(event);
}
// defer touches on QQS to shade while shade is collapsing. Added margin for error
@@ -2429,7 +2534,7 @@
if (!mSplitShadeEnabled
&& computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
mShadeLog.logMotionEvent(event,
- "handleQsTouch: QQS touched while shade collapsing, QS tracking disabled");
+ "handleQsTouch: shade touched while collapsing, QS tracking disabled");
mQsTracking = false;
}
if (!mQsExpandImmediate && mQsTracking) {
@@ -2727,7 +2832,7 @@
} else if (statusBarState == KEYGUARD
|| statusBarState == StatusBarState.SHADE_LOCKED) {
mKeyguardBottomArea.setVisibility(View.VISIBLE);
- if (!mIsToLockscreenTransitionRunning) {
+ if (!mIsOcclusionTransitionRunning) {
mKeyguardBottomArea.setAlpha(1f);
}
} else {
@@ -2935,7 +3040,7 @@
// left bounds can ignore insets, it should always reach the edge of the screen
return 0;
} else {
- return mNotificationStackScrollLayoutController.getLeft();
+ return mNotificationStackScrollLayoutController.getLeft() + mDisplayLeftInset;
}
}
@@ -2943,7 +3048,7 @@
if (mIsFullWidth) {
return mView.getRight() + mDisplayRightInset;
} else {
- return mNotificationStackScrollLayoutController.getRight();
+ return mNotificationStackScrollLayoutController.getRight() + mDisplayLeftInset;
}
}
@@ -3042,20 +3147,15 @@
mQsClipBottom,
radius,
qsVisible && !mSplitShadeEnabled);
+ mKeyguardInteractor.setQuickSettingsVisible(mQsVisible);
}
// The padding on this area is large enough that we can use a cheaper clipping strategy
mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
- if (!qsVisible && mSplitShadeEnabled) {
- // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
- // be visible, otherwise you can see the bounds once swiping up to see bouncer
- mScrimController.setNotificationsBounds(0, 0, 0, 0);
- } else {
- // Increase the height of the notifications scrim when not in split shade
- // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
- // in this case they are rendered off-screen
- final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
- mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
- }
+ // Increase the height of the notifications scrim when not in split shade
+ // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
+ // in this case they are rendered off-screen
+ final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
+ mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
if (mSplitShadeEnabled) {
mKeyguardStatusBarViewController.setNoTopClipping();
@@ -3067,8 +3167,8 @@
// Convert global clipping coordinates to local ones,
// relative to NotificationStackScrollLayout
- int nsslLeft = left - mNotificationStackScrollLayoutController.getLeft();
- int nsslRight = right - mNotificationStackScrollLayoutController.getLeft();
+ int nsslLeft = calculateNsslLeft(left);
+ int nsslRight = calculateNsslRight(right);
int nsslTop = getNotificationsClippingTopBounds(top);
int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
int bottomRadius = mSplitShadeEnabled ? radius : 0;
@@ -3077,6 +3177,22 @@
nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
}
+ private int calculateNsslLeft(int nsslLeftAbsolute) {
+ int left = nsslLeftAbsolute - mNotificationStackScrollLayoutController.getLeft();
+ if (mIsFullWidth) {
+ return left;
+ }
+ return left - mDisplayLeftInset;
+ }
+
+ private int calculateNsslRight(int nsslRightAbsolute) {
+ int right = nsslRightAbsolute - mNotificationStackScrollLayoutController.getLeft();
+ if (mIsFullWidth) {
+ return right;
+ }
+ return right - mDisplayLeftInset;
+ }
+
private int getNotificationsClippingTopBounds(int qsTop) {
if (mSplitShadeEnabled && mExpandingFromHeadsUp) {
// in split shade nssl has extra top margin so clipping at top 0 is not enough, we need
@@ -3100,6 +3216,12 @@
private int calculateQsBottomPosition(float qsExpansionFraction) {
if (mTransitioningToFullShadeProgress > 0.0f) {
return mTransitionToFullShadeQSPosition;
+ } else if (mSplitShadeEnabled) {
+ // in split shade - outside lockscreen transition handled above - we simply jump between
+ // two qs expansion values - either shade is closed and qs expansion is 0 or shade is
+ // open and qs expansion is 1
+ int qsBottomTarget = mQs.getDesiredHeight() + mLargeScreenShadeHeaderHeight;
+ return qsExpansionFraction > 0 ? qsBottomTarget : 0;
} else {
int qsBottomYFrom = (int) getHeaderTranslation() + mQs.getQsMinExpansionHeight();
int expandedTopMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : 0;
@@ -3596,7 +3718,7 @@
}
private void updateNotificationTranslucency() {
- if (mIsToLockscreenTransitionRunning) {
+ if (mIsOcclusionTransitionRunning) {
return;
}
float alpha = 1f;
@@ -3654,7 +3776,7 @@
}
private void updateKeyguardBottomAreaAlpha() {
- if (mIsToLockscreenTransitionRunning) {
+ if (mIsOcclusionTransitionRunning) {
return;
}
// There are two possible panel expansion behaviors:
@@ -3664,7 +3786,8 @@
// change due to "unlock hint animation." In this case, fading out the bottom area
// would also hide the message that says "swipe to unlock," we don't want to do that.
float expansionAlpha = MathUtils.map(
- isUnlockHintRunning() ? 0 : KeyguardBouncer.ALPHA_EXPANSION_THRESHOLD, 1f, 0f, 1f,
+ isUnlockHintRunning() ? 0 : KeyguardBouncerConstants.ALPHA_EXPANSION_THRESHOLD, 1f,
+ 0f, 1f,
getExpandedFraction());
float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
alpha *= mBottomAreaShadeAlpha;
@@ -4093,9 +4216,7 @@
}
private void updateStatusBarIcons() {
- boolean showIconsWhenExpanded =
- (isPanelVisibleBecauseOfHeadsUp() || mIsFullWidth)
- && getExpandedHeight() < getOpeningHeight();
+ boolean showIconsWhenExpanded = getExpandedHeight() < getOpeningHeight();
if (showIconsWhenExpanded && isOnKeyguard()) {
showIconsWhenExpanded = false;
}
@@ -4162,7 +4283,7 @@
&& mHeadsUpAppearanceController.shouldBeVisible()) {
return false;
}
- return !mIsFullWidth || !mShowIconsWhenExpanded;
+ return !mShowIconsWhenExpanded;
}
private void onQsPanelScrollChanged(int scrollY) {
@@ -4474,6 +4595,7 @@
ipw.print("mDownY="); ipw.println(mDownY);
ipw.print("mDisplayTopInset="); ipw.println(mDisplayTopInset);
ipw.print("mDisplayRightInset="); ipw.println(mDisplayRightInset);
+ ipw.print("mDisplayLeftInset="); ipw.println(mDisplayLeftInset);
ipw.print("mLargeScreenShadeHeaderHeight="); ipw.println(mLargeScreenShadeHeaderHeight);
ipw.print("mSplitShadeNotificationsScrimMarginBottom=");
ipw.println(mSplitShadeNotificationsScrimMarginBottom);
@@ -4836,8 +4958,12 @@
beginJankMonitoring();
}
mInitialOffsetOnTouch = expandedHeight;
- mInitialExpandY = newY;
- mInitialExpandX = newX;
+ if (!mTracking || isFullyCollapsed()) {
+ mInitialExpandY = newY;
+ mInitialExpandX = newX;
+ } else {
+ mShadeLog.d("not setting mInitialExpandY in startExpandMotion");
+ }
mInitialTouchFromKeyguard = mKeyguardStateController.isShowing();
if (startTracking) {
mTouchSlopExceeded = true;
@@ -4902,7 +5028,7 @@
mUpdateFlingVelocity = vel;
}
} else if (!mCentralSurfaces.isBouncerShowing()
- && !mStatusBarKeyguardViewManager.isShowingAlternateBouncer()
+ && !mAlternateBouncerInteractor.isVisibleState()
&& !mKeyguardStateController.isKeyguardGoingAway()) {
onEmptySpaceClick();
onTrackingStopped(true);
@@ -5211,6 +5337,11 @@
}
}
+ /** Returns whether a shade or QS expansion animation is running */
+ private boolean isShadeOrQsHeightAnimationRunning() {
+ return mHeightAnimator != null && !mHintAnimationRunning && !mIsSpringBackAnimation;
+ }
+
/**
* Phase 2: Bounce down.
*/
@@ -5370,10 +5501,6 @@
InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
}
- private float getExpansionFraction() {
- return mExpandedFraction;
- }
-
private ShadeExpansionStateManager getShadeExpansionStateManager() {
return mShadeExpansionStateManager;
}
@@ -5443,7 +5570,7 @@
@Override
public void flingTopOverscroll(float velocity, boolean open) {
- // in split shade mode we want to expand/collapse QS only when touch happens within QS
+ // in split shade touches affect QS only when touch happens within QS
if (isSplitShadeAndTouchXOutsideQs(mInitialTouchX)) {
return;
}
@@ -5850,6 +5977,7 @@
Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes);
mDisplayTopInset = combinedInsets.top;
mDisplayRightInset = combinedInsets.right;
+ mDisplayLeftInset = combinedInsets.left;
mNavigationBarBottomHeight = insets.getStableInsetBottom();
updateMaxHeadsUpTranslation();
@@ -5886,7 +6014,7 @@
mCurrentPanelState = state;
}
- private Consumer<Float> toLockscreenTransitionAlpha(
+ private Consumer<Float> setTransitionAlpha(
NotificationStackScrollLayoutController stackScroller) {
return (Float alpha) -> {
mKeyguardStatusViewController.setAlpha(alpha);
@@ -5904,7 +6032,7 @@
};
}
- private Consumer<Float> toLockscreenTransitionY(
+ private Consumer<Float> setTransitionY(
NotificationStackScrollLayoutController stackScroller) {
return (Float translationY) -> {
mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false);
@@ -6019,8 +6147,12 @@
+ " false");
return true;
}
- mInitialExpandY = y;
- mInitialExpandX = x;
+ if (!mTracking || isFullyCollapsed()) {
+ mInitialExpandY = y;
+ mInitialExpandX = x;
+ } else {
+ mShadeLog.d("not setting mInitialExpandY in onInterceptTouch");
+ }
mTouchStartedInEmptyArea = !isInContentBounds(x, y);
mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
mMotionAborted = false;
@@ -6209,7 +6341,8 @@
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN
+ || event.getActionMasked() == MotionEvent.ACTION_MOVE) {
mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
mIgnoreXTouchSlop = true;
}
@@ -6228,8 +6361,7 @@
mCollapsedAndHeadsUpOnDown =
isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
addMovement(event);
- boolean regularHeightAnimationRunning = mHeightAnimator != null
- && !mHintAnimationRunning && !mIsSpringBackAnimation;
+ boolean regularHeightAnimationRunning = isShadeOrQsHeightAnimationRunning();
if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) {
mTouchSlopExceeded = regularHeightAnimationRunning
|| mTouchSlopExceededBeforeDown;
@@ -6272,7 +6404,7 @@
mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction);
}
addMovement(event);
- if (!isFullyCollapsed()) {
+ if (!isFullyCollapsed() && !isOnKeyguard()) {
maybeVibrateOnOpening(true /* openingWithTouch */);
}
float h = y - mInitialExpandY;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 8314ec7..156e4fd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -19,6 +19,7 @@
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
@@ -52,13 +53,13 @@
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.DumpsysTableLogger;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -72,10 +73,8 @@
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -89,6 +88,7 @@
Dumpable, ConfigurationListener {
private static final String TAG = "NotificationShadeWindowController";
+ private static final int MAX_STATE_CHANGES_BUFFER_SIZE = 100;
private final Context mContext;
private final WindowManager mWindowManager;
@@ -108,7 +108,7 @@
private boolean mHasTopUi;
private boolean mHasTopUiChanged;
private float mScreenBrightnessDoze;
- private final State mCurrentState = new State();
+ private final NotificationShadeWindowState mCurrentState = new NotificationShadeWindowState();
private OtherwisedCollapsedListener mListener;
private ForcePluginOpenListener mForcePluginOpenListener;
private Consumer<Integer> mScrimsVisibilityListener;
@@ -125,6 +125,9 @@
private int mDeferWindowLayoutParams;
private boolean mLastKeyguardRotationAllowed;
+ private final NotificationShadeWindowState.Buffer mStateBuffer =
+ new NotificationShadeWindowState.Buffer(MAX_STATE_CHANGES_BUFFER_SIZE);
+
@Inject
public NotificationShadeWindowControllerImpl(Context context, WindowManager windowManager,
IActivityManager activityManager, DozeParameters dozeParameters,
@@ -210,8 +213,8 @@
@VisibleForTesting
void onShadeExpansionFullyChanged(Boolean isExpanded) {
- if (mCurrentState.mPanelExpanded != isExpanded) {
- mCurrentState.mPanelExpanded = isExpanded;
+ if (mCurrentState.panelExpanded != isExpanded) {
+ mCurrentState.panelExpanded = isExpanded;
apply(mCurrentState);
}
}
@@ -251,6 +254,7 @@
mLp.setTitle("NotificationShade");
mLp.packageName = mContext.getPackageName();
mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ mLp.privateFlags |= PRIVATE_FLAG_OPTIMIZE_MEASURE;
// We use BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE here, however, there is special logic in
// window manager which disables the transient show behavior.
@@ -296,10 +300,10 @@
mNotificationShadeView.setSystemUiVisibility(vis);
}
- private void applyKeyguardFlags(State state) {
- final boolean keyguardOrAod = state.mKeyguardShowing
- || (state.mDozing && mDozeParameters.getAlwaysOn());
- if ((keyguardOrAod && !state.mBackdropShowing && !state.mLightRevealScrimOpaque)
+ private void applyKeyguardFlags(NotificationShadeWindowState state) {
+ final boolean keyguardOrAod = state.keyguardShowing
+ || (state.dozing && mDozeParameters.getAlwaysOn());
+ if ((keyguardOrAod && !state.mediaBackdropShowing && !state.lightRevealScrimOpaque)
|| mKeyguardViewMediator.isAnimatingBetweenKeyguardAndSurfaceBehind()) {
// Show the wallpaper if we're on keyguard/AOD and the wallpaper is not occluded by a
// solid backdrop. Also, show it if we are currently animating between the
@@ -310,28 +314,31 @@
mLpChanged.flags &= ~LayoutParams.FLAG_SHOW_WALLPAPER;
}
- if (state.mDozing) {
+ if (state.dozing) {
mLpChanged.privateFlags |= LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
} else {
mLpChanged.privateFlags &= ~LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
}
if (mKeyguardPreferredRefreshRate > 0) {
- boolean onKeyguard = state.mStatusBarState == StatusBarState.KEYGUARD
- && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
+ boolean onKeyguard = state.statusBarState == StatusBarState.KEYGUARD
+ && !state.keyguardFadingAway && !state.keyguardGoingAway;
if (onKeyguard
&& mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) {
+ // both max and min display refresh rate must be set to take effect:
mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate;
+ mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate;
} else {
mLpChanged.preferredMaxDisplayRefreshRate = 0;
+ mLpChanged.preferredMinDisplayRefreshRate = 0;
}
Trace.setCounter("display_set_preferred_refresh_rate",
(long) mKeyguardPreferredRefreshRate);
} else if (mKeyguardMaxRefreshRate > 0) {
boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled()
- && state.mStatusBarState == StatusBarState.KEYGUARD
- && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
- if (state.mDozing || bypassOnKeyguard) {
+ && state.statusBarState == StatusBarState.KEYGUARD
+ && !state.keyguardFadingAway && !state.keyguardGoingAway;
+ if (state.dozing || bypassOnKeyguard) {
mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardMaxRefreshRate;
} else {
mLpChanged.preferredMaxDisplayRefreshRate = 0;
@@ -340,7 +347,7 @@
(long) mLpChanged.preferredMaxDisplayRefreshRate);
}
- if (state.mBouncerShowing && !isDebuggable()) {
+ if (state.bouncerShowing && !isDebuggable()) {
mLpChanged.flags |= LayoutParams.FLAG_SECURE;
} else {
mLpChanged.flags &= ~LayoutParams.FLAG_SECURE;
@@ -351,8 +358,8 @@
return Build.IS_DEBUGGABLE;
}
- private void adjustScreenOrientation(State state) {
- if (state.mBouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.mDozing) {
+ private void adjustScreenOrientation(NotificationShadeWindowState state) {
+ if (state.bouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.dozing) {
if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) {
mLpChanged.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
} else {
@@ -363,10 +370,10 @@
}
}
- private void applyFocusableFlag(State state) {
- boolean panelFocusable = state.mNotificationShadeFocusable && state.mPanelExpanded;
- if (state.mBouncerShowing && (state.mKeyguardOccluded || state.mKeyguardNeedsInput)
- || ENABLE_REMOTE_INPUT && state.mRemoteInputActive
+ private void applyFocusableFlag(NotificationShadeWindowState state) {
+ boolean panelFocusable = state.notificationShadeFocusable && state.panelExpanded;
+ if (state.bouncerShowing && (state.keyguardOccluded || state.keyguardNeedsInput)
+ || ENABLE_REMOTE_INPUT && state.remoteInputActive
// Make the panel focusable if we're doing the screen off animation, since the light
// reveal scrim is drawing in the panel and should consume touch events so that they
// don't go to the app behind.
@@ -376,7 +383,7 @@
} else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
// Make sure to remove FLAG_ALT_FOCUSABLE_IM when keyguard needs input.
- if (state.mKeyguardNeedsInput && state.isKeyguardShowingAndNotOccluded()) {
+ if (state.keyguardNeedsInput && state.isKeyguardShowingAndNotOccluded()) {
mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
} else {
mLpChanged.flags |= LayoutParams.FLAG_ALT_FOCUSABLE_IM;
@@ -387,19 +394,19 @@
}
}
- private void applyForceShowNavigationFlag(State state) {
- if (state.mPanelExpanded || state.mBouncerShowing
- || ENABLE_REMOTE_INPUT && state.mRemoteInputActive) {
+ private void applyForceShowNavigationFlag(NotificationShadeWindowState state) {
+ if (state.panelExpanded || state.bouncerShowing
+ || ENABLE_REMOTE_INPUT && state.remoteInputActive) {
mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
} else {
mLpChanged.privateFlags &= ~LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
}
}
- private void applyVisibility(State state) {
+ private void applyVisibility(NotificationShadeWindowState state) {
boolean visible = isExpanded(state);
mLogger.logApplyVisibility(visible);
- if (state.mForcePluginOpen) {
+ if (state.forcePluginOpen) {
if (mListener != null) {
mListener.setWouldOtherwiseCollapse(visible);
}
@@ -415,16 +422,16 @@
}
}
- private boolean isExpanded(State state) {
- return !state.mForceCollapsed && (state.isKeyguardShowingAndNotOccluded()
- || state.mPanelVisible || state.mKeyguardFadingAway || state.mBouncerShowing
- || state.mHeadsUpShowing
- || state.mScrimsVisibility != ScrimController.TRANSPARENT)
- || state.mBackgroundBlurRadius > 0
- || state.mLaunchingActivity;
+ private boolean isExpanded(NotificationShadeWindowState state) {
+ return !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded()
+ || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
+ || state.headsUpNotificationShowing
+ || state.scrimsVisibility != ScrimController.TRANSPARENT)
+ || state.backgroundBlurRadius > 0
+ || state.launchingActivityFromNotification;
}
- private void applyFitsSystemWindows(State state) {
+ private void applyFitsSystemWindows(NotificationShadeWindowState state) {
boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
if (mNotificationShadeView != null
&& mNotificationShadeView.getFitsSystemWindows() != fitsSystemWindows) {
@@ -433,21 +440,21 @@
}
}
- private void applyUserActivityTimeout(State state) {
+ private void applyUserActivityTimeout(NotificationShadeWindowState state) {
if (state.isKeyguardShowingAndNotOccluded()
- && state.mStatusBarState == StatusBarState.KEYGUARD
- && !state.mQsExpanded) {
- mLpChanged.userActivityTimeout = state.mBouncerShowing
+ && state.statusBarState == StatusBarState.KEYGUARD
+ && !state.qsExpanded) {
+ mLpChanged.userActivityTimeout = state.bouncerShowing
? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout;
} else {
mLpChanged.userActivityTimeout = -1;
}
}
- private void applyInputFeatures(State state) {
+ private void applyInputFeatures(NotificationShadeWindowState state) {
if (state.isKeyguardShowingAndNotOccluded()
- && state.mStatusBarState == StatusBarState.KEYGUARD
- && !state.mQsExpanded && !state.mForceUserActivity) {
+ && state.statusBarState == StatusBarState.KEYGUARD
+ && !state.qsExpanded && !state.forceUserActivity) {
mLpChanged.inputFeatures |=
LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
} else {
@@ -456,7 +463,7 @@
}
}
- private void applyStatusBarColorSpaceAgnosticFlag(State state) {
+ private void applyStatusBarColorSpaceAgnosticFlag(NotificationShadeWindowState state) {
if (!isExpanded(state)) {
mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
} else {
@@ -482,8 +489,8 @@
applyWindowLayoutParams();
}
- private void apply(State state) {
- mLogger.logNewState(state);
+ private void apply(NotificationShadeWindowState state) {
+ logState(state);
applyKeyguardFlags(state);
applyFocusableFlag(state);
applyForceShowNavigationFlag(state);
@@ -512,6 +519,38 @@
notifyStateChangedCallbacks();
}
+ private void logState(NotificationShadeWindowState state) {
+ mStateBuffer.insert(
+ state.keyguardShowing,
+ state.keyguardOccluded,
+ state.keyguardNeedsInput,
+ state.panelVisible,
+ state.panelExpanded,
+ state.notificationShadeFocusable,
+ state.bouncerShowing,
+ state.keyguardFadingAway,
+ state.keyguardGoingAway,
+ state.qsExpanded,
+ state.headsUpNotificationShowing,
+ state.lightRevealScrimOpaque,
+ state.forceWindowCollapsed,
+ state.forceDozeBrightness,
+ state.forceUserActivity,
+ state.launchingActivityFromNotification,
+ state.mediaBackdropShowing,
+ state.wallpaperSupportsAmbientMode,
+ state.windowNotTouchable,
+ state.componentsForcingTopUi,
+ state.forceOpenTokens,
+ state.statusBarState,
+ state.remoteInputActive,
+ state.forcePluginOpen,
+ state.dozing,
+ state.scrimsVisibility,
+ state.backgroundBlurRadius
+ );
+ }
+
@Override
public void notifyStateChangedCallbacks() {
// Copy callbacks to separate ArrayList to avoid concurrent modification
@@ -520,36 +559,37 @@
.filter(Objects::nonNull)
.collect(Collectors.toList());
for (StatusBarWindowCallback cb : activeCallbacks) {
- cb.onStateChanged(mCurrentState.mKeyguardShowing,
- mCurrentState.mKeyguardOccluded,
- mCurrentState.mBouncerShowing,
- mCurrentState.mDozing,
- mCurrentState.mPanelExpanded);
+ cb.onStateChanged(mCurrentState.keyguardShowing,
+ mCurrentState.keyguardOccluded,
+ mCurrentState.bouncerShowing,
+ mCurrentState.dozing,
+ mCurrentState.panelExpanded,
+ mCurrentState.dreaming);
}
}
- private void applyModalFlag(State state) {
- if (state.mHeadsUpShowing) {
+ private void applyModalFlag(NotificationShadeWindowState state) {
+ if (state.headsUpNotificationShowing) {
mLpChanged.flags |= LayoutParams.FLAG_NOT_TOUCH_MODAL;
} else {
mLpChanged.flags &= ~LayoutParams.FLAG_NOT_TOUCH_MODAL;
}
}
- private void applyBrightness(State state) {
- if (state.mForceDozeBrightness) {
+ private void applyBrightness(NotificationShadeWindowState state) {
+ if (state.forceDozeBrightness) {
mLpChanged.screenBrightness = mScreenBrightnessDoze;
} else {
mLpChanged.screenBrightness = LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
}
}
- private void applyHasTopUi(State state) {
- mHasTopUiChanged = !state.mComponentsForcingTopUi.isEmpty() || isExpanded(state);
+ private void applyHasTopUi(NotificationShadeWindowState state) {
+ mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state);
}
- private void applyNotTouchable(State state) {
- if (state.mNotTouchable) {
+ private void applyNotTouchable(NotificationShadeWindowState state) {
+ if (state.windowNotTouchable) {
mLpChanged.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
} else {
mLpChanged.flags &= ~LayoutParams.FLAG_NOT_TOUCHABLE;
@@ -571,88 +611,88 @@
@Override
public void setKeyguardShowing(boolean showing) {
- mCurrentState.mKeyguardShowing = showing;
+ mCurrentState.keyguardShowing = showing;
apply(mCurrentState);
}
@Override
public void setKeyguardOccluded(boolean occluded) {
- mCurrentState.mKeyguardOccluded = occluded;
+ mCurrentState.keyguardOccluded = occluded;
apply(mCurrentState);
}
@Override
public void setKeyguardNeedsInput(boolean needsInput) {
- mCurrentState.mKeyguardNeedsInput = needsInput;
+ mCurrentState.keyguardNeedsInput = needsInput;
apply(mCurrentState);
}
@Override
public void setPanelVisible(boolean visible) {
- if (mCurrentState.mPanelVisible == visible
- && mCurrentState.mNotificationShadeFocusable == visible) {
+ if (mCurrentState.panelVisible == visible
+ && mCurrentState.notificationShadeFocusable == visible) {
return;
}
mLogger.logShadeVisibleAndFocusable(visible);
- mCurrentState.mPanelVisible = visible;
- mCurrentState.mNotificationShadeFocusable = visible;
+ mCurrentState.panelVisible = visible;
+ mCurrentState.notificationShadeFocusable = visible;
apply(mCurrentState);
}
@Override
public void setNotificationShadeFocusable(boolean focusable) {
mLogger.logShadeFocusable(focusable);
- mCurrentState.mNotificationShadeFocusable = focusable;
+ mCurrentState.notificationShadeFocusable = focusable;
apply(mCurrentState);
}
@Override
public void setBouncerShowing(boolean showing) {
- mCurrentState.mBouncerShowing = showing;
+ mCurrentState.bouncerShowing = showing;
apply(mCurrentState);
}
@Override
public void setBackdropShowing(boolean showing) {
- mCurrentState.mBackdropShowing = showing;
+ mCurrentState.mediaBackdropShowing = showing;
apply(mCurrentState);
}
@Override
public void setKeyguardFadingAway(boolean keyguardFadingAway) {
- mCurrentState.mKeyguardFadingAway = keyguardFadingAway;
+ mCurrentState.keyguardFadingAway = keyguardFadingAway;
apply(mCurrentState);
}
private void onQsExpansionChanged(Boolean expanded) {
- mCurrentState.mQsExpanded = expanded;
+ mCurrentState.qsExpanded = expanded;
apply(mCurrentState);
}
@Override
public void setForceUserActivity(boolean forceUserActivity) {
- mCurrentState.mForceUserActivity = forceUserActivity;
+ mCurrentState.forceUserActivity = forceUserActivity;
apply(mCurrentState);
}
@Override
public void setLaunchingActivity(boolean launching) {
- mCurrentState.mLaunchingActivity = launching;
+ mCurrentState.launchingActivityFromNotification = launching;
apply(mCurrentState);
}
@Override
public boolean isLaunchingActivity() {
- return mCurrentState.mLaunchingActivity;
+ return mCurrentState.launchingActivityFromNotification;
}
@Override
public void setScrimsVisibility(int scrimsVisibility) {
- if (scrimsVisibility == mCurrentState.mScrimsVisibility) {
+ if (scrimsVisibility == mCurrentState.scrimsVisibility) {
return;
}
boolean wasExpanded = isExpanded(mCurrentState);
- mCurrentState.mScrimsVisibility = scrimsVisibility;
+ mCurrentState.scrimsVisibility = scrimsVisibility;
if (wasExpanded != isExpanded(mCurrentState)) {
apply(mCurrentState);
}
@@ -666,31 +706,31 @@
*/
@Override
public void setBackgroundBlurRadius(int backgroundBlurRadius) {
- if (mCurrentState.mBackgroundBlurRadius == backgroundBlurRadius) {
+ if (mCurrentState.backgroundBlurRadius == backgroundBlurRadius) {
return;
}
- mCurrentState.mBackgroundBlurRadius = backgroundBlurRadius;
+ mCurrentState.backgroundBlurRadius = backgroundBlurRadius;
apply(mCurrentState);
}
@Override
public void setHeadsUpShowing(boolean showing) {
- mCurrentState.mHeadsUpShowing = showing;
+ mCurrentState.headsUpNotificationShowing = showing;
apply(mCurrentState);
}
@Override
public void setLightRevealScrimOpaque(boolean opaque) {
- if (mCurrentState.mLightRevealScrimOpaque == opaque) {
+ if (mCurrentState.lightRevealScrimOpaque == opaque) {
return;
}
- mCurrentState.mLightRevealScrimOpaque = opaque;
+ mCurrentState.lightRevealScrimOpaque = opaque;
apply(mCurrentState);
}
@Override
public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
- mCurrentState.mWallpaperSupportsAmbientMode = supportsAmbientMode;
+ mCurrentState.wallpaperSupportsAmbientMode = supportsAmbientMode;
apply(mCurrentState);
}
@@ -698,7 +738,7 @@
* @param state The {@link StatusBarStateController} of the status bar.
*/
private void setStatusBarState(int state) {
- mCurrentState.mStatusBarState = state;
+ mCurrentState.statusBarState = state;
apply(mCurrentState);
}
@@ -709,13 +749,13 @@
*/
@Override
public void setForceWindowCollapsed(boolean force) {
- mCurrentState.mForceCollapsed = force;
+ mCurrentState.forceWindowCollapsed = force;
apply(mCurrentState);
}
@Override
public void onRemoteInputActive(boolean remoteInputActive) {
- mCurrentState.mRemoteInputActive = remoteInputActive;
+ mCurrentState.remoteInputActive = remoteInputActive;
apply(mCurrentState);
}
@@ -725,32 +765,38 @@
*/
@Override
public void setForceDozeBrightness(boolean forceDozeBrightness) {
- if (mCurrentState.mForceDozeBrightness == forceDozeBrightness) {
+ if (mCurrentState.forceDozeBrightness == forceDozeBrightness) {
return;
}
- mCurrentState.mForceDozeBrightness = forceDozeBrightness;
+ mCurrentState.forceDozeBrightness = forceDozeBrightness;
apply(mCurrentState);
}
@Override
public void setDozing(boolean dozing) {
- mCurrentState.mDozing = dozing;
+ mCurrentState.dozing = dozing;
+ apply(mCurrentState);
+ }
+
+ @Override
+ public void setDreaming(boolean dreaming) {
+ mCurrentState.dreaming = dreaming;
apply(mCurrentState);
}
@Override
public void setForcePluginOpen(boolean forceOpen, Object token) {
if (forceOpen) {
- mCurrentState.mForceOpenTokens.add(token);
+ mCurrentState.forceOpenTokens.add(token);
} else {
- mCurrentState.mForceOpenTokens.remove(token);
+ mCurrentState.forceOpenTokens.remove(token);
}
- final boolean previousForceOpenState = mCurrentState.mForcePluginOpen;
- mCurrentState.mForcePluginOpen = !mCurrentState.mForceOpenTokens.isEmpty();
- if (previousForceOpenState != mCurrentState.mForcePluginOpen) {
+ final boolean previousForceOpenState = mCurrentState.forcePluginOpen;
+ mCurrentState.forcePluginOpen = !mCurrentState.forceOpenTokens.isEmpty();
+ if (previousForceOpenState != mCurrentState.forcePluginOpen) {
apply(mCurrentState);
if (mForcePluginOpenListener != null) {
- mForcePluginOpenListener.onChange(mCurrentState.mForcePluginOpen);
+ mForcePluginOpenListener.onChange(mCurrentState.forcePluginOpen);
}
}
}
@@ -760,12 +806,12 @@
*/
@Override
public boolean getForcePluginOpen() {
- return mCurrentState.mForcePluginOpen;
+ return mCurrentState.forcePluginOpen;
}
@Override
public void setNotTouchable(boolean notTouchable) {
- mCurrentState.mNotTouchable = notTouchable;
+ mCurrentState.windowNotTouchable = notTouchable;
apply(mCurrentState);
}
@@ -774,7 +820,7 @@
*/
@Override
public boolean getPanelExpanded() {
- return mCurrentState.mPanelExpanded;
+ return mCurrentState.panelExpanded;
}
@Override
@@ -797,11 +843,16 @@
if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) {
mNotificationShadeView.getViewRootImpl().dump(" ", pw);
}
+ new DumpsysTableLogger(
+ TAG,
+ NotificationShadeWindowState.TABLE_HEADERS,
+ mStateBuffer.toList()
+ ).printTableData(pw);
}
@Override
public boolean isShowingWallpaper() {
- return !mCurrentState.mBackdropShowing;
+ return !mCurrentState.mediaBackdropShowing;
}
@Override
@@ -831,7 +882,7 @@
*/
@Override
public void setKeyguardGoingAway(boolean goingAway) {
- mCurrentState.mKeyguardGoingAway = goingAway;
+ mCurrentState.keyguardGoingAway = goingAway;
apply(mCurrentState);
}
@@ -843,87 +894,13 @@
@Override
public void setRequestTopUi(boolean requestTopUi, String componentTag) {
if (requestTopUi) {
- mCurrentState.mComponentsForcingTopUi.add(componentTag);
+ mCurrentState.componentsForcingTopUi.add(componentTag);
} else {
- mCurrentState.mComponentsForcingTopUi.remove(componentTag);
+ mCurrentState.componentsForcingTopUi.remove(componentTag);
}
apply(mCurrentState);
}
- private static class State {
- boolean mKeyguardShowing;
- boolean mKeyguardOccluded;
- boolean mKeyguardNeedsInput;
- boolean mPanelVisible;
- boolean mPanelExpanded;
- boolean mNotificationShadeFocusable;
- boolean mBouncerShowing;
- boolean mKeyguardFadingAway;
- boolean mKeyguardGoingAway;
- boolean mQsExpanded;
- boolean mHeadsUpShowing;
- boolean mLightRevealScrimOpaque;
- boolean mForceCollapsed;
- boolean mForceDozeBrightness;
- boolean mForceUserActivity;
- boolean mLaunchingActivity;
- boolean mBackdropShowing;
- boolean mWallpaperSupportsAmbientMode;
- boolean mNotTouchable;
- Set<String> mComponentsForcingTopUi = new HashSet<>();
- Set<Object> mForceOpenTokens = new HashSet<>();
-
- /**
- * The status bar state from {@link CentralSurfaces}.
- */
- int mStatusBarState;
-
- boolean mRemoteInputActive;
- boolean mForcePluginOpen;
- boolean mDozing;
- int mScrimsVisibility;
- int mBackgroundBlurRadius;
-
- private boolean isKeyguardShowingAndNotOccluded() {
- return mKeyguardShowing && !mKeyguardOccluded;
- }
-
- @Override
- public String toString() {
- return new StringBuilder()
- .append("State{")
- .append(" mKeyguardShowing=").append(mKeyguardShowing)
- .append(", mKeyguardOccluded=").append(mKeyguardOccluded)
- .append(", mKeyguardNeedsInput=").append(mKeyguardNeedsInput)
- .append(", mPanelVisible=").append(mPanelVisible)
- .append(", mPanelExpanded=").append(mPanelExpanded)
- .append(", mNotificationShadeFocusable=").append(mNotificationShadeFocusable)
- .append(", mBouncerShowing=").append(mBouncerShowing)
- .append(", mKeyguardFadingAway=").append(mKeyguardFadingAway)
- .append(", mKeyguardGoingAway=").append(mKeyguardGoingAway)
- .append(", mQsExpanded=").append(mQsExpanded)
- .append(", mHeadsUpShowing=").append(mHeadsUpShowing)
- .append(", mLightRevealScrimOpaque=").append(mLightRevealScrimOpaque)
- .append(", mForceCollapsed=").append(mForceCollapsed)
- .append(", mForceDozeBrightness=").append(mForceDozeBrightness)
- .append(", mForceUserActivity=").append(mForceUserActivity)
- .append(", mLaunchingActivity=").append(mLaunchingActivity)
- .append(", mBackdropShowing=").append(mBackdropShowing)
- .append(", mWallpaperSupportsAmbientMode=")
- .append(mWallpaperSupportsAmbientMode)
- .append(", mNotTouchable=").append(mNotTouchable)
- .append(", mComponentsForcingTopUi=").append(mComponentsForcingTopUi)
- .append(", mForceOpenTokens=").append(mForceOpenTokens)
- .append(", mStatusBarState=").append(mStatusBarState)
- .append(", mRemoteInputActive=").append(mRemoteInputActive)
- .append(", mForcePluginOpen=").append(mForcePluginOpen)
- .append(", mDozing=").append(mDozing)
- .append(", mScrimsVisibility=").append(mScrimsVisibility)
- .append(", mBackgroundBlurRadius=").append(mBackgroundBlurRadius)
- .append('}').toString();
- }
- }
-
private final StateListener mStateListener = new StateListener() {
@Override
public void onStateChanged(int newState) {
@@ -934,5 +911,10 @@
public void onDozingChanged(boolean isDozing) {
setDozing(isDozing);
}
+
+ @Override
+ public void onDreamingChanged(boolean isDreaming) {
+ setDreaming(isDreaming);
+ }
};
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
new file mode 100644
index 0000000..fed9b84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.systemui.dump.DumpsysTableLogger
+import com.android.systemui.dump.Row
+import com.android.systemui.plugins.util.RingBuffer
+import com.android.systemui.shade.NotificationShadeWindowState.Buffer
+import com.android.systemui.statusbar.StatusBarState
+
+/**
+ * Represents state of shade window, used by [NotificationShadeWindowControllerImpl]. Contains
+ * nested class [Buffer] for pretty table logging in bug reports.
+ */
+class NotificationShadeWindowState(
+ @JvmField var keyguardShowing: Boolean = false,
+ @JvmField var keyguardOccluded: Boolean = false,
+ @JvmField var keyguardNeedsInput: Boolean = false,
+ @JvmField var panelVisible: Boolean = false,
+ /** shade panel is expanded (expansion fraction > 0) */
+ @JvmField var panelExpanded: Boolean = false,
+ @JvmField var notificationShadeFocusable: Boolean = false,
+ @JvmField var bouncerShowing: Boolean = false,
+ @JvmField var keyguardFadingAway: Boolean = false,
+ @JvmField var keyguardGoingAway: Boolean = false,
+ @JvmField var qsExpanded: Boolean = false,
+ @JvmField var headsUpNotificationShowing: Boolean = false,
+ @JvmField var lightRevealScrimOpaque: Boolean = false,
+ @JvmField var forceWindowCollapsed: Boolean = false,
+ @JvmField var forceDozeBrightness: Boolean = false,
+ // TODO: forceUserActivity seems to be unused, delete?
+ @JvmField var forceUserActivity: Boolean = false,
+ @JvmField var launchingActivityFromNotification: Boolean = false,
+ @JvmField var mediaBackdropShowing: Boolean = false,
+ @JvmField var wallpaperSupportsAmbientMode: Boolean = false,
+ @JvmField var windowNotTouchable: Boolean = false,
+ @JvmField var componentsForcingTopUi: MutableSet<String> = mutableSetOf(),
+ @JvmField var forceOpenTokens: MutableSet<Any> = mutableSetOf(),
+ /** one of [StatusBarState] */
+ @JvmField var statusBarState: Int = 0,
+ @JvmField var remoteInputActive: Boolean = false,
+ @JvmField var forcePluginOpen: Boolean = false,
+ @JvmField var dozing: Boolean = false,
+ @JvmField var dreaming: Boolean = false,
+ @JvmField var scrimsVisibility: Int = 0,
+ @JvmField var backgroundBlurRadius: Int = 0,
+) {
+
+ fun isKeyguardShowingAndNotOccluded(): Boolean {
+ return keyguardShowing && !keyguardOccluded
+ }
+
+ /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */
+ val asStringList: List<String> by lazy {
+ listOf(
+ keyguardShowing.toString(),
+ keyguardOccluded.toString(),
+ keyguardNeedsInput.toString(),
+ panelVisible.toString(),
+ panelExpanded.toString(),
+ notificationShadeFocusable.toString(),
+ bouncerShowing.toString(),
+ keyguardFadingAway.toString(),
+ keyguardGoingAway.toString(),
+ qsExpanded.toString(),
+ headsUpNotificationShowing.toString(),
+ lightRevealScrimOpaque.toString(),
+ forceWindowCollapsed.toString(),
+ forceDozeBrightness.toString(),
+ forceUserActivity.toString(),
+ launchingActivityFromNotification.toString(),
+ mediaBackdropShowing.toString(),
+ wallpaperSupportsAmbientMode.toString(),
+ windowNotTouchable.toString(),
+ componentsForcingTopUi.toString(),
+ forceOpenTokens.toString(),
+ StatusBarState.toString(statusBarState),
+ remoteInputActive.toString(),
+ forcePluginOpen.toString(),
+ dozing.toString(),
+ scrimsVisibility.toString(),
+ backgroundBlurRadius.toString()
+ )
+ }
+
+ /**
+ * [RingBuffer] to store [NotificationShadeWindowState]. After the buffer is full, it will
+ * recycle old events.
+ */
+ class Buffer(capacity: Int) {
+
+ private val buffer = RingBuffer(capacity) { NotificationShadeWindowState() }
+
+ /** Insert a new element in the buffer. */
+ fun insert(
+ keyguardShowing: Boolean,
+ keyguardOccluded: Boolean,
+ keyguardNeedsInput: Boolean,
+ panelVisible: Boolean,
+ panelExpanded: Boolean,
+ notificationShadeFocusable: Boolean,
+ bouncerShowing: Boolean,
+ keyguardFadingAway: Boolean,
+ keyguardGoingAway: Boolean,
+ qsExpanded: Boolean,
+ headsUpShowing: Boolean,
+ lightRevealScrimOpaque: Boolean,
+ forceCollapsed: Boolean,
+ forceDozeBrightness: Boolean,
+ forceUserActivity: Boolean,
+ launchingActivity: Boolean,
+ backdropShowing: Boolean,
+ wallpaperSupportsAmbientMode: Boolean,
+ notTouchable: Boolean,
+ componentsForcingTopUi: MutableSet<String>,
+ forceOpenTokens: MutableSet<Any>,
+ statusBarState: Int,
+ remoteInputActive: Boolean,
+ forcePluginOpen: Boolean,
+ dozing: Boolean,
+ scrimsVisibility: Int,
+ backgroundBlurRadius: Int,
+ ) {
+ buffer.advance().apply {
+ this.keyguardShowing = keyguardShowing
+ this.keyguardOccluded = keyguardOccluded
+ this.keyguardNeedsInput = keyguardNeedsInput
+ this.panelVisible = panelVisible
+ this.panelExpanded = panelExpanded
+ this.notificationShadeFocusable = notificationShadeFocusable
+ this.bouncerShowing = bouncerShowing
+ this.keyguardFadingAway = keyguardFadingAway
+ this.keyguardGoingAway = keyguardGoingAway
+ this.qsExpanded = qsExpanded
+ this.headsUpNotificationShowing = headsUpShowing
+ this.lightRevealScrimOpaque = lightRevealScrimOpaque
+ this.forceWindowCollapsed = forceCollapsed
+ this.forceDozeBrightness = forceDozeBrightness
+ this.forceUserActivity = forceUserActivity
+ this.launchingActivityFromNotification = launchingActivity
+ this.mediaBackdropShowing = backdropShowing
+ this.wallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode
+ this.windowNotTouchable = notTouchable
+ this.componentsForcingTopUi.clear()
+ this.componentsForcingTopUi.addAll(componentsForcingTopUi)
+ this.forceOpenTokens.clear()
+ this.forceOpenTokens.addAll(forceOpenTokens)
+ this.statusBarState = statusBarState
+ this.remoteInputActive = remoteInputActive
+ this.forcePluginOpen = forcePluginOpen
+ this.dozing = dozing
+ this.scrimsVisibility = scrimsVisibility
+ this.backgroundBlurRadius = backgroundBlurRadius
+ }
+ }
+
+ /**
+ * Returns the content of the buffer (sorted from latest to newest).
+ *
+ * @see [NotificationShadeWindowState.asStringList]
+ */
+ fun toList(): List<Row> {
+ return buffer.asSequence().map { it.asStringList }.toList()
+ }
+ }
+
+ companion object {
+ /** Headers for dumping a table using [DumpsysTableLogger]. */
+ @JvmField
+ val TABLE_HEADERS =
+ listOf(
+ "keyguardShowing",
+ "keyguardOccluded",
+ "keyguardNeedsInput",
+ "panelVisible",
+ "panelExpanded",
+ "notificationShadeFocusable",
+ "bouncerShowing",
+ "keyguardFadingAway",
+ "keyguardGoingAway",
+ "qsExpanded",
+ "headsUpShowing",
+ "lightRevealScrimOpaque",
+ "forceCollapsed",
+ "forceDozeBrightness",
+ "forceUserActivity",
+ "launchingActivity",
+ "backdropShowing",
+ "wallpaperSupportsAmbientMode",
+ "notTouchable",
+ "componentsForcingTopUi",
+ "forceOpenTokens",
+ "statusBarState",
+ "remoteInputActive",
+ "forcePluginOpen",
+ "dozing",
+ "scrimsVisibility",
+ "backgroundBlurRadius"
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index 6acf417..1f0cbf9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -58,6 +58,7 @@
import com.android.internal.view.FloatingActionMode;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import com.android.systemui.R;
+import com.android.systemui.compose.ComposeFacade;
/**
* Combined keyguard and notification panel view. Also holding backdrop and scrims.
@@ -149,6 +150,18 @@
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setWillNotDraw(!DEBUG);
+
+ if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ ComposeFacade.INSTANCE.composeInitializer().onAttachedToWindow(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ ComposeFacade.INSTANCE.composeInitializer().onDetachedFromWindow(this);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 5c1ddd6..60fa865 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,6 +16,8 @@
package com.android.systemui.shade;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+
import android.app.StatusBarManager;
import android.media.AudioManager;
import android.media.session.MediaSessionLegacyHelper;
@@ -39,6 +41,10 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.statusbar.DragDownHelper;
@@ -57,6 +63,7 @@
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import java.io.PrintWriter;
+import java.util.function.Consumer;
import javax.inject.Inject;
@@ -79,6 +86,7 @@
private final AmbientState mAmbientState;
private final PulsingGestureListener mPulsingGestureListener;
private final NotificationInsetsController mNotificationInsetsController;
+ private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private GestureDetector mPulsingWakeupGestureHandler;
private View mBrightnessMirror;
@@ -96,6 +104,13 @@
private final ShadeExpansionStateManager mShadeExpansionStateManager;
private boolean mIsTrackingBarGesture = false;
+ private boolean mIsOcclusionTransitionRunning = false;
+
+ private final Consumer<TransitionStep> mLockscreenToDreamingTransition =
+ (TransitionStep step) -> {
+ mIsOcclusionTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
@Inject
public NotificationShadeWindowViewController(
@@ -119,7 +134,9 @@
PulsingGestureListener pulsingGestureListener,
FeatureFlags featureFlags,
KeyguardBouncerViewModel keyguardBouncerViewModel,
- KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory
+ KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
+ AlternateBouncerInteractor alternateBouncerInteractor,
+ KeyguardTransitionInteractor keyguardTransitionInteractor
) {
mLockscreenShadeTransitionController = transitionController;
mFalsingCollector = falsingCollector;
@@ -139,14 +156,18 @@
mAmbientState = ambientState;
mPulsingGestureListener = pulsingGestureListener;
mNotificationInsetsController = notificationInsetsController;
+ mAlternateBouncerInteractor = alternateBouncerInteractor;
// This view is not part of the newly inflated expanded status bar.
mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
- if (featureFlags.isEnabled(Flags.MODERN_BOUNCER)) {
- KeyguardBouncerViewBinder.bind(
- mView.findViewById(R.id.keyguard_bouncer_container),
- keyguardBouncerViewModel,
- keyguardBouncerComponentFactory);
+ KeyguardBouncerViewBinder.bind(
+ mView.findViewById(R.id.keyguard_bouncer_container),
+ keyguardBouncerViewModel,
+ keyguardBouncerComponentFactory);
+
+ if (featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION)) {
+ collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(),
+ mLockscreenToDreamingTransition);
}
}
@@ -215,6 +236,10 @@
return true;
}
+ if (mIsOcclusionTransitionRunning) {
+ return false;
+ }
+
mFalsingCollector.onTouchEvent(ev);
mPulsingWakeupGestureHandler.onTouchEvent(ev);
mStatusBarKeyguardViewManager.onTouch(ev);
@@ -292,7 +317,7 @@
return true;
}
- if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.isVisibleState()) {
// capture all touches if the alt auth bouncer is showing
return true;
}
@@ -330,7 +355,7 @@
handled = !mService.isPulsing();
}
- if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.isVisibleState()) {
// eat the touch
handled = true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 85b259e..de02115 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -31,6 +31,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.fragments.FragmentService
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
@@ -54,6 +55,7 @@
private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
private val shadeExpansionStateManager: ShadeExpansionStateManager,
private val featureFlags: FeatureFlags,
+ private val fragmentService: FragmentService,
@Main private val delayableExecutor: DelayableExecutor
) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
@@ -128,6 +130,7 @@
mView.setInsetsChangedListener(delayedInsetSetter)
mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
mView.setConfigurationChangedListener { updateResources() }
+ fragmentService.getFragmentHostManager(mView).addTagListener(QS.TAG, mView)
}
override fun onViewDetached() {
@@ -136,6 +139,7 @@
mView.removeOnInsetsChangedListener()
mView.removeQSFragmentAttachedListener()
mView.setConfigurationChangedListener(null)
+ fragmentService.getFragmentHostManager(mView).removeTagListener(QS.TAG, mView)
}
fun updateResources() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
index 02316b7..f73dde6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
@@ -29,7 +29,6 @@
import androidx.constraintlayout.widget.ConstraintSet;
import com.android.systemui.R;
-import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.statusbar.notification.AboveShelfObserver;
@@ -133,18 +132,6 @@
}
@Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- FragmentHostManager.get(this).addTagListener(QS.TAG, this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- FragmentHostManager.get(this).removeTagListener(QS.TAG, this);
- }
-
- @Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mInsetsChangedListener.accept(insets);
return insets;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index db70065..b42bdaa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -19,7 +19,6 @@
import android.hardware.display.AmbientDisplayConfiguration
import android.os.PowerManager
import android.os.SystemClock
-import android.os.UserHandle
import android.provider.Settings
import android.view.GestureDetector
import android.view.MotionEvent
@@ -29,6 +28,7 @@
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
import com.android.systemui.tuner.TunerService
@@ -54,6 +54,7 @@
private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
private val statusBarStateController: StatusBarStateController,
private val shadeLogger: ShadeLogger,
+ userTracker: UserTracker,
tunerService: TunerService,
dumpManager: DumpManager
) : GestureDetector.SimpleOnGestureListener(), Dumpable {
@@ -65,10 +66,10 @@
when (key) {
Settings.Secure.DOZE_DOUBLE_TAP_GESTURE ->
doubleTapEnabled = ambientDisplayConfiguration.doubleTapGestureEnabled(
- UserHandle.USER_CURRENT)
+ userTracker.userId)
Settings.Secure.DOZE_TAP_SCREEN_GESTURE ->
singleTapEnabled = ambientDisplayConfiguration.tapGestureEnabled(
- UserHandle.USER_CURRENT)
+ userTracker.userId)
}
}
tunerService.addTunable(tunable,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 5fedbeb..11617be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -36,16 +36,9 @@
buffer.log(TAG, LogLevel.DEBUG, msg)
}
- private inline fun log(
- logLevel: LogLevel,
- initializer: LogMessage.() -> Unit,
- noinline printer: LogMessage.() -> String
- ) {
- buffer.log(TAG, logLevel, initializer, printer)
- }
-
fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
- log(
+ buffer.log(
+ TAG,
LogLevel.VERBOSE,
{ double1 = h.toDouble() },
{ "onQsIntercept: move action, QS tracking enabled. h = $double1" }
@@ -62,7 +55,8 @@
keyguardShowing: Boolean,
qsExpansionEnabled: Boolean
) {
- log(
+ buffer.log(
+ TAG,
LogLevel.VERBOSE,
{
int1 = initialTouchY.toInt()
@@ -82,7 +76,8 @@
}
fun logMotionEvent(event: MotionEvent, message: String) {
- log(
+ buffer.log(
+ TAG,
LogLevel.VERBOSE,
{
str1 = message
@@ -99,7 +94,8 @@
}
fun logMotionEventStatusBarState(event: MotionEvent, statusBarState: Int, message: String) {
- log(
+ buffer.log(
+ TAG,
LogLevel.VERBOSE,
{
str1 = message
@@ -128,25 +124,33 @@
tracking: Boolean,
dragDownPxAmount: Float,
) {
- log(LogLevel.VERBOSE, {
- str1 = message
- double1 = fraction.toDouble()
- bool1 = expanded
- bool2 = tracking
- long1 = dragDownPxAmount.toLong()
- }, {
- "$str1 fraction=$double1,expanded=$bool1," +
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ str1 = message
+ double1 = fraction.toDouble()
+ bool1 = expanded
+ bool2 = tracking
+ long1 = dragDownPxAmount.toLong()
+ },
+ {
+ "$str1 fraction=$double1,expanded=$bool1," +
"tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount"
- })
+ }
+ )
}
fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) {
- log(LogLevel.VERBOSE, {
- bool1 = hasVibratedOnOpen
- double1 = fraction.toDouble()
- }, {
- "hasVibratedOnOpen=$bool1, expansionFraction=$double1"
- })
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ bool1 = hasVibratedOnOpen
+ double1 = fraction.toDouble()
+ },
+ { "hasVibratedOnOpen=$bool1, expansionFraction=$double1" }
+ )
}
fun logQsExpansionChanged(
@@ -159,42 +163,56 @@
qsAnimatorExpand: Boolean,
animatingQs: Boolean
) {
- log(LogLevel.VERBOSE, {
- str1 = message
- bool1 = qsExpanded
- int1 = qsMinExpansionHeight
- int2 = qsMaxExpansionHeight
- bool2 = stackScrollerOverscrolling
- bool3 = dozing
- bool4 = qsAnimatorExpand
- // 0 = false, 1 = true
- long1 = animatingQs.compareTo(false).toLong()
- }, {
- "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," +
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ str1 = message
+ bool1 = qsExpanded
+ int1 = qsMinExpansionHeight
+ int2 = qsMaxExpansionHeight
+ bool2 = stackScrollerOverscrolling
+ bool3 = dozing
+ bool4 = qsAnimatorExpand
+ // 0 = false, 1 = true
+ long1 = animatingQs.compareTo(false).toLong()
+ },
+ {
+ "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," +
"stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," +
"animatingQs=$long1"
- })
+ }
+ )
}
fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) {
- log(LogLevel.DEBUG, {
- bool1 = isDozing
- bool2 = singleTapEnabled
- bool3 = isNotDocked
- }, {
- "PulsingGestureListener#onSingleTapUp all of this must true for single " +
- "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3"
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ bool1 = isDozing
+ bool2 = singleTapEnabled
+ bool3 = isNotDocked
+ },
+ {
+ "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+ "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3"
})
}
fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) {
- log(LogLevel.DEBUG, {
- bool1 = proximityIsNotNear
- bool2 = isNotFalseTap
- }, {
- "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ bool1 = proximityIsNotNear
+ bool2 = isNotFalseTap
+ },
+ {
+ "PulsingGestureListener#onSingleTapUp all of this must true for single " +
"tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2"
- })
+ }
+ )
}
fun logNotInterceptingTouchInstantExpanding(
@@ -202,13 +220,18 @@
notificationsDragEnabled: Boolean,
touchDisabled: Boolean
) {
- log(LogLevel.VERBOSE, {
- bool1 = instantExpanding
- bool2 = notificationsDragEnabled
- bool3 = touchDisabled
- }, {
- "NPVC not intercepting touch, instantExpanding: $bool1, " +
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ bool1 = instantExpanding
+ bool2 = notificationsDragEnabled
+ bool3 = touchDisabled
+ },
+ {
+ "NPVC not intercepting touch, instantExpanding: $bool1, " +
"!notificationsDragEnabled: $bool2, touchDisabled: $bool3"
- })
+ }
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
index c6a6e87..9851625 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt
@@ -32,11 +32,21 @@
ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) {
fun logApplyingWindowLayoutParams(lp: WindowManager.LayoutParams) {
- log(DEBUG, { str1 = lp.toString() }, { "Applying new window layout params: $str1" })
+ buffer.log(
+ TAG,
+ DEBUG,
+ { str1 = lp.toString() },
+ { "Applying new window layout params: $str1" }
+ )
}
fun logNewState(state: Any) {
- log(DEBUG, { str1 = state.toString() }, { "Applying new state: $str1" })
+ buffer.log(
+ TAG,
+ DEBUG,
+ { str1 = state.toString() },
+ { "Applying new state: $str1" }
+ )
}
private inline fun log(
@@ -48,11 +58,16 @@
}
fun logApplyVisibility(visible: Boolean) {
- log(DEBUG, { bool1 = visible }, { "Updating visibility, should be visible : $bool1" })
+ buffer.log(
+ TAG,
+ DEBUG,
+ { bool1 = visible },
+ { "Updating visibility, should be visible : $bool1" })
}
fun logShadeVisibleAndFocusable(visible: Boolean) {
- log(
+ buffer.log(
+ TAG,
DEBUG,
{ bool1 = visible },
{ "Updating shade, should be visible and focusable: $bool1" }
@@ -60,6 +75,11 @@
}
fun logShadeFocusable(focusable: Boolean) {
- log(DEBUG, { bool1 = focusable }, { "Updating shade, should be focusable : $bool1" })
+ buffer.log(
+ TAG,
+ DEBUG,
+ { bool1 = focusable },
+ { "Updating shade, should be focusable : $bool1" }
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
new file mode 100644
index 0000000..ab0d6e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.smartspace.config
+
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
+
+class BcSmartspaceConfigProvider(private val featureFlags: FeatureFlags) :
+ BcSmartspaceConfigPlugin {
+ override val isDefaultDateWeatherDisabled: Boolean
+ get() = featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index b02a45a..641131e 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -18,7 +18,6 @@
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.smartspace.SmartspacePrecondition
import com.android.systemui.smartspace.SmartspaceTargetFilter
-import com.android.systemui.smartspace.filters.LockscreenAndDreamTargetFilter
import com.android.systemui.smartspace.preconditions.LockscreenPrecondition
import dagger.Binds
import dagger.BindsOptionalOf
@@ -35,11 +34,6 @@
const val DREAM_SMARTSPACE_DATA_PLUGIN = "dreams_smartspace_data_plugin"
/**
- * The lockscreen smartspace target filter.
- */
- const val LOCKSCREEN_SMARTSPACE_TARGET_FILTER = "lockscreen_smartspace_target_filter"
-
- /**
* The dream smartspace target filter.
*/
const val DREAM_SMARTSPACE_TARGET_FILTER = "dream_smartspace_target_filter"
@@ -48,6 +42,16 @@
* The precondition for dream smartspace
*/
const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition"
+
+ /**
+ * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
+ */
+ const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin"
+
+ /**
+ * The BcSmartspaceDataPlugin for the standalone weather.
+ */
+ const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
}
@BindsOptionalOf
@@ -59,12 +63,6 @@
abstract fun optionalDreamsBcSmartspaceDataPlugin(): BcSmartspaceDataPlugin?
@Binds
- @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER)
- abstract fun provideLockscreenSmartspaceTargetFilter(
- filter: LockscreenAndDreamTargetFilter?
- ): SmartspaceTargetFilter?
-
- @Binds
@Named(DREAM_SMARTSPACE_PRECONDITION)
abstract fun bindSmartspacePrecondition(
lockscreenPrecondition: LockscreenPrecondition?
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt b/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt
index 1302ec9..88e8ad9 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt
@@ -15,8 +15,6 @@
*/
package com.android.systemui.smartspace.preconditions
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.smartspace.SmartspacePrecondition
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.util.concurrency.Execution
@@ -24,11 +22,9 @@
/**
* {@link LockscreenPrecondition} covers the conditions that must be met before Smartspace can be
- * used over lockscreen. These conditions include the device being provisioned with a setup user
- * and the Smartspace feature flag enabled.
+ * used over lockscreen. These conditions include the device being provisioned with a setup user.
*/
class LockscreenPrecondition @Inject constructor(
- private val featureFlags: FeatureFlags,
private val deviceProvisionedController: DeviceProvisionedController,
private val execution: Execution
) : SmartspacePrecondition {
@@ -90,6 +86,6 @@
override fun conditionsMet(): Boolean {
execution.assertIsMainThread()
- return featureFlags.isEnabled(Flags.SMARTSPACE) && deviceReady
+ return deviceReady
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
index 87c12c2..6577cf6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedButton.java
@@ -20,10 +20,21 @@
import android.util.AttributeSet;
import android.widget.Button;
+import com.android.systemui.animation.LaunchableView;
+import com.android.systemui.animation.LaunchableViewDelegate;
+
+import kotlin.Unit;
+
/**
* A Button which doesn't have overlapping drawing commands
*/
-public class AlphaOptimizedButton extends Button {
+public class AlphaOptimizedButton extends Button implements LaunchableView {
+ private LaunchableViewDelegate mDelegate = new LaunchableViewDelegate(this,
+ (visibility) -> {
+ super.setVisibility(visibility);
+ return Unit.INSTANCE;
+ });
+
public AlphaOptimizedButton(Context context) {
super(context);
}
@@ -45,4 +56,14 @@
public boolean hasOverlappingRendering() {
return false;
}
+
+ @Override
+ public void setShouldBlockVisibilityChanges(boolean block) {
+ mDelegate.setShouldBlockVisibilityChanges(block);
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mDelegate.setVisibility(visibility);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
index 662f70e..438b0f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java
@@ -36,10 +36,6 @@
visibility -> {
super.setVisibility(visibility);
return Unit.INSTANCE;
- },
- visibility -> {
- super.setTransitionVisibility(visibility);
- return Unit.INSTANCE;
});
public AlphaOptimizedFrameLayout(Context context) {
@@ -73,9 +69,4 @@
public void setVisibility(int visibility) {
mLaunchableViewDelegate.setVisibility(visibility);
}
-
- @Override
- public void setTransitionVisibility(int visibility) {
- mLaunchableViewDelegate.setTransitionVisibility(visibility);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 750d004..f2e729d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -20,11 +20,8 @@
import static android.app.StatusBarManager.DISABLE_NONE;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
-import static com.android.systemui.statusbar.phone.CentralSurfacesImpl.ONLY_CORE_APPS;
-
import android.annotation.Nullable;
import android.app.ITransientNotificationCallback;
import android.app.StatusBarManager;
@@ -40,17 +37,19 @@
import android.hardware.biometrics.IBiometricContextListener;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
-import android.hardware.display.DisplayManager;
import android.hardware.fingerprint.IUdfpsHbmListener;
import android.inputmethodservice.InputMethodService.BackDispositionMode;
import android.media.INearbyMediaDevicesProvider;
import android.media.MediaRoute2Info;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.os.RemoteException;
import android.util.Pair;
import android.util.SparseArray;
@@ -60,6 +59,7 @@
import android.view.WindowInsetsController.Behavior;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.statusbar.IAddTileResultCallback;
@@ -70,6 +70,7 @@
import com.android.internal.util.GcUtils;
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.dump.DumpHandler;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.policy.CallbackController;
@@ -89,8 +90,7 @@
* are coalesced, note that they are all idempotent.
*/
public class CommandQueue extends IStatusBar.Stub implements
- CallbackController<Callbacks>,
- DisplayManager.DisplayListener {
+ CallbackController<Callbacks> {
private static final String TAG = CommandQueue.class.getSimpleName();
private static final int INDEX_MASK = 0xffff;
@@ -168,6 +168,7 @@
private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT;
private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT;
private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
+ private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT;
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -191,6 +192,7 @@
private ProtoTracer mProtoTracer;
private final @Nullable CommandRegistry mRegistry;
private final @Nullable DumpHandler mDumpHandler;
+ private final @Nullable DisplayTracker mDisplayTracker;
/**
* These methods are called back on the main thread.
@@ -334,7 +336,7 @@
/**
* @see IStatusBar#setBiometicContextListener(IBiometricContextListener)
*/
- default void setBiometicContextListener(IBiometricContextListener listener) {
+ default void setBiometricContextListener(IBiometricContextListener listener) {
}
/**
@@ -350,7 +352,7 @@
}
/**
- * @see DisplayManager.DisplayListener#onDisplayRemoved(int)
+ * @see DisplayTracker.Callback#onDisplayRemoved(int)
*/
default void onDisplayRemoved(int displayId) {
}
@@ -492,14 +494,21 @@
* @see IStatusBar#enterStageSplitFromRunningApp
*/
default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
+
+ /**
+ * @see IStatusBar#showMediaOutputSwitcher
+ */
+ default void showMediaOutputSwitcher(String packageName) {}
}
- public CommandQueue(Context context) {
- this(context, null, null, null);
+ @VisibleForTesting
+ public CommandQueue(Context context, DisplayTracker displayTracker) {
+ this(context, displayTracker, null, null, null);
}
public CommandQueue(
Context context,
+ DisplayTracker displayTracker,
ProtoTracer protoTracer,
CommandRegistry registry,
DumpHandler dumpHandler
@@ -507,36 +516,30 @@
mProtoTracer = protoTracer;
mRegistry = registry;
mDumpHandler = dumpHandler;
- context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler);
+ mDisplayTracker = displayTracker;
+ mDisplayTracker.addDisplayChangeCallback(new DisplayTracker.Callback() {
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ synchronized (mLock) {
+ mDisplayDisabled.remove(displayId);
+ }
+ // This callback is registered with {@link #mHandler} that already posts to run
+ // on main thread, so it is safe to dispatch directly.
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ mCallbacks.get(i).onDisplayRemoved(displayId);
+ }
+ }
+ }, new HandlerExecutor(mHandler));
// We always have default display.
- setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE);
+ setDisabled(mDisplayTracker.getDefaultDisplayId(), DISABLE_NONE, DISABLE2_NONE);
}
- @Override
- public void onDisplayAdded(int displayId) { }
-
- @Override
- public void onDisplayRemoved(int displayId) {
- synchronized (mLock) {
- mDisplayDisabled.remove(displayId);
- }
- // This callback is registered with {@link #mHandler} that already posts to run on main
- // thread, so it is safe to dispatch directly.
- for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- mCallbacks.get(i).onDisplayRemoved(displayId);
- }
- }
-
- @Override
- public void onDisplayChanged(int displayId) { }
-
// TODO(b/118592525): add multi-display support if needed.
public boolean panelsEnabled() {
- final int disabled1 = getDisabled1(DEFAULT_DISPLAY);
- final int disabled2 = getDisabled2(DEFAULT_DISPLAY);
+ final int disabled1 = getDisabled1(mDisplayTracker.getDefaultDisplayId());
+ final int disabled2 = getDisabled2(mDisplayTracker.getDefaultDisplayId());
return (disabled1 & StatusBarManager.DISABLE_EXPAND) == 0
- && (disabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0
- && !ONLY_CORE_APPS;
+ && (disabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) == 0;
}
@Override
@@ -1262,6 +1265,19 @@
}
@Override
+ public void showMediaOutputSwitcher(String packageName) {
+ int callingUid = Binder.getCallingUid();
+ if (callingUid != 0 && callingUid != Process.SYSTEM_UID) {
+ throw new SecurityException("Call only allowed from system server.");
+ }
+ synchronized (mLock) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = packageName;
+ mHandler.obtainMessage(MSG_SHOW_MEDIA_OUTPUT_SWITCHER, args).sendToTarget();
+ }
+ }
+
+ @Override
public void requestAddTile(
@NonNull ComponentName componentName,
@NonNull CharSequence appName,
@@ -1583,7 +1599,7 @@
}
case MSG_SET_BIOMETRICS_LISTENER:
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).setBiometicContextListener(
+ mCallbacks.get(i).setBiometricContextListener(
(IBiometricContextListener) msg.obj);
}
break;
@@ -1777,6 +1793,13 @@
mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj);
}
break;
+ case MSG_SHOW_MEDIA_OUTPUT_SWITCHER:
+ args = (SomeArgs) msg.obj;
+ String clientPackageName = (String) args.arg1;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName);
+ }
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index 63179da..5adb58b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -77,6 +77,12 @@
*/
public static void fadeOut(View view, float fadeOutAmount, boolean remap) {
view.animate().cancel();
+
+ // Don't fade out if already not visible.
+ if (view.getAlpha() == 0.0f) {
+ return;
+ }
+
if (fadeOutAmount == 1.0f && view.getVisibility() != View.GONE) {
view.setVisibility(View.INVISIBLE);
} else if (view.getVisibility() == View.INVISIBLE) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java
new file mode 100644
index 0000000..a797d4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsModule.java
@@ -0,0 +1,40 @@
+/*
+ * 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.statusbar;
+
+import android.content.BroadcastReceiver;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+
+/**
+ * Module for {@link com.android.systemui.KeyboardShortcutsReceiver}.
+ */
+@Module
+public abstract class KeyboardShortcutsModule {
+
+ /**
+ *
+ */
+ @Binds
+ @IntoMap
+ @ClassKey(KeyboardShortcutsReceiver.class)
+ public abstract BroadcastReceiver bindKeyboardShortcutsReceiver(
+ KeyboardShortcutsReceiver broadcastReceiver);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 6a658b6..6481855 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -41,7 +41,9 @@
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED;
import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
+import static com.android.systemui.plugins.log.LogLevel.ERROR;
+import android.app.AlarmManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -90,12 +92,14 @@
import com.android.systemui.keyguard.KeyguardIndication;
import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController;
import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.AlarmTimeout;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.wakelock.SettableWakeLock;
import com.android.systemui.util.wakelock.WakeLock;
@@ -125,10 +129,8 @@
private static final String TAG = "KeyguardIndication";
private static final boolean DEBUG_CHARGING_SPEED = false;
- private static final int MSG_HIDE_TRANSIENT = 1;
- private static final int MSG_SHOW_ACTION_TO_UNLOCK = 2;
- private static final int MSG_HIDE_BIOMETRIC_MESSAGE = 3;
- private static final int MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON = 4;
+ private static final int MSG_SHOW_ACTION_TO_UNLOCK = 1;
+ private static final int MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON = 2;
private static final long TRANSIENT_BIOMETRIC_ERROR_TIMEOUT = 1300;
public static final long DEFAULT_HIDE_DELAY_MS =
3500 + KeyguardIndicationTextView.Y_IN_DURATION;
@@ -155,6 +157,7 @@
private final KeyguardBypassController mKeyguardBypassController;
private final AccessibilityManager mAccessibilityManager;
private final Handler mHandler;
+ private final AlternateBouncerInteractor mAlternateBouncerInteractor;
@VisibleForTesting
public KeyguardIndicationRotateTextViewController mRotateTextViewController;
@@ -209,6 +212,11 @@
};
private boolean mFaceLockedOutThisAuthSession;
+ // Use AlarmTimeouts to guarantee that the events are handled even if scheduled and
+ // triggered while the device is asleep
+ private final AlarmTimeout mHideTransientMessageHandler;
+ private final AlarmTimeout mHideBiometricMessageHandler;
+
/**
* Creates a new KeyguardIndicationController and registers callbacks.
*/
@@ -234,7 +242,10 @@
KeyguardBypassController keyguardBypassController,
AccessibilityManager accessibilityManager,
FaceHelpMessageDeferral faceHelpMessageDeferral,
- KeyguardLogger keyguardLogger) {
+ KeyguardLogger keyguardLogger,
+ AlternateBouncerInteractor alternateBouncerInteractor,
+ AlarmManager alarmManager
+ ) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
mDevicePolicyManager = devicePolicyManager;
@@ -256,6 +267,7 @@
mScreenLifecycle = screenLifecycle;
mKeyguardLogger = keyguardLogger;
mScreenLifecycle.addObserver(mScreenObserver);
+ mAlternateBouncerInteractor = alternateBouncerInteractor;
mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
@@ -268,17 +280,26 @@
mHandler = new Handler(mainLooper) {
@Override
public void handleMessage(Message msg) {
- if (msg.what == MSG_HIDE_TRANSIENT) {
- hideTransientIndication();
- } else if (msg.what == MSG_SHOW_ACTION_TO_UNLOCK) {
+ if (msg.what == MSG_SHOW_ACTION_TO_UNLOCK) {
showActionToUnlock();
- } else if (msg.what == MSG_HIDE_BIOMETRIC_MESSAGE) {
- hideBiometricMessage();
} else if (msg.what == MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON) {
mBiometricErrorMessageToShowOnScreenOn = null;
}
}
};
+
+ mHideTransientMessageHandler = new AlarmTimeout(
+ alarmManager,
+ this::hideTransientIndication,
+ TAG,
+ mHandler
+ );
+ mHideBiometricMessageHandler = new AlarmTimeout(
+ alarmManager,
+ this::hideBiometricMessage,
+ TAG,
+ mHandler
+ );
}
/** Call this after construction to finish setting up the instance. */
@@ -330,6 +351,8 @@
*/
public void destroy() {
mHandler.removeCallbacksAndMessages(null);
+ mHideBiometricMessageHandler.cancel();
+ mHideTransientMessageHandler.cancel();
mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
}
@@ -674,7 +697,7 @@
if (visible) {
// If this is called after an error message was already shown, we should not clear it.
// Otherwise the error message won't be shown
- if (!mHandler.hasMessages(MSG_HIDE_TRANSIENT)) {
+ if (!mHideTransientMessageHandler.isScheduled()) {
hideTransientIndication();
}
updateDeviceEntryIndication(false);
@@ -722,16 +745,14 @@
* Hides transient indication in {@param delayMs}.
*/
public void hideTransientIndicationDelayed(long delayMs) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_HIDE_TRANSIENT), delayMs);
+ mHideTransientMessageHandler.schedule(delayMs, AlarmTimeout.MODE_RESCHEDULE_IF_SCHEDULED);
}
/**
* Hides biometric indication in {@param delayMs}.
*/
public void hideBiometricMessageDelayed(long delayMs) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_HIDE_BIOMETRIC_MESSAGE), delayMs);
+ mHideBiometricMessageHandler.schedule(delayMs, AlarmTimeout.MODE_RESCHEDULE_IF_SCHEDULED);
}
/**
@@ -746,7 +767,6 @@
*/
private void showTransientIndication(CharSequence transientIndication) {
mTransientIndication = transientIndication;
- mHandler.removeMessages(MSG_HIDE_TRANSIENT);
hideTransientIndicationDelayed(DEFAULT_HIDE_DELAY_MS);
updateTransient();
@@ -772,7 +792,6 @@
mBiometricMessageFollowUp = biometricMessageFollowUp;
mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
- mHandler.removeMessages(MSG_HIDE_BIOMETRIC_MESSAGE);
hideBiometricMessageDelayed(
mBiometricMessageFollowUp != null
? IMPORTANT_MSG_MIN_DURATION * 2
@@ -786,7 +805,7 @@
if (mBiometricMessage != null || mBiometricMessageFollowUp != null) {
mBiometricMessage = null;
mBiometricMessageFollowUp = null;
- mHandler.removeMessages(MSG_HIDE_BIOMETRIC_MESSAGE);
+ mHideBiometricMessageHandler.cancel();
updateBiometricMessage();
}
}
@@ -797,7 +816,7 @@
public void hideTransientIndication() {
if (mTransientIndication != null) {
mTransientIndication = null;
- mHandler.removeMessages(MSG_HIDE_TRANSIENT);
+ mHideTransientMessageHandler.cancel();
updateTransient();
}
}
@@ -928,7 +947,7 @@
}
if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
- if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.isVisibleState()) {
return; // udfps affordance is highlighted, no need to show action to unlock
} else if (mKeyguardUpdateMonitor.isFaceEnrolled()
&& !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) {
@@ -1028,7 +1047,7 @@
mChargingTimeRemaining = mPowerPluggedIn
? mBatteryInfo.computeChargeTimeRemaining() : -1;
} catch (RemoteException e) {
- mKeyguardLogger.logException(e, "Error calling IBatteryStats");
+ mKeyguardLogger.log(TAG, ERROR, "Error calling IBatteryStats", e);
mChargingTimeRemaining = -1;
}
updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 905cc3f..f565f3d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -509,9 +509,14 @@
* If secure with redaction: Show bouncer, go to unlocked shade.
* If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
*
+ * Split shade is special case and [needsQSAnimation] will be always overridden to true.
+ * That's because handheld shade will automatically follow notifications animation, but that's
+ * not the case for split shade.
+ *
* @param expandView The view to expand after going to the shade
* @param needsQSAnimation if this needs the quick settings to slide in from the top or if
- * that's already handled separately
+ * that's already handled separately. This argument will be ignored on
+ * split shade as there QS animation can't be handled separately.
*/
@JvmOverloads
fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) {
@@ -519,7 +524,7 @@
logger.logTryGoToLockedShade(isKeyguard)
if (isKeyguard) {
val animationHandler: ((Long) -> Unit)?
- if (needsQSAnimation) {
+ if (needsQSAnimation || useSplitShade) {
// Let's use the default animation
animationHandler = null
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 56b689e..7d0ac18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -290,7 +290,8 @@
false,
null,
0,
- false
+ false,
+ 0
);
}
return ranking;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 8f9365c..99081e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -65,8 +65,6 @@
import com.android.systemui.util.DumpUtilsKt;
import com.android.systemui.util.ListenerSet;
-import dagger.Lazy;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -74,6 +72,8 @@
import java.util.Optional;
import java.util.function.Consumer;
+import dagger.Lazy;
+
/**
* Class for handling remote input state over a set of notifications. This class handles things
* like keeping notifications temporarily that were cancelled as a response to a remote input
@@ -465,8 +465,7 @@
riv.getController().setRemoteInput(input);
riv.getController().setRemoteInputs(inputs);
riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
- ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
- riv.focusAnimated(parent);
+ riv.focusAnimated();
if (userMessageContent != null) {
riv.setEditTextContent(userMessageContent);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 0b1807d..2ca0b00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -143,6 +143,9 @@
/** Sets the state of whether sysui is dozing or not. */
default void setDozing(boolean dozing) {}
+ /** Sets the state of whether sysui is dreaming or not. */
+ default void setDreaming(boolean dreaming) {}
+
/** Sets the state of whether plugin open is forced or not. */
default void setForcePluginOpen(boolean forcePluginOpen, Object token) {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 186e6dc..784e2d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -129,6 +129,11 @@
private boolean mIsDozing;
/**
+ * If the device is currently dreaming or not.
+ */
+ private boolean mIsDreaming;
+
+ /**
* If the status bar is currently expanded or not.
*/
private boolean mIsExpanded;
@@ -294,6 +299,29 @@
}
@Override
+ public boolean setIsDreaming(boolean isDreaming) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "setIsDreaming:" + isDreaming);
+ }
+ if (mIsDreaming == isDreaming) {
+ return false;
+ }
+
+ mIsDreaming = isDreaming;
+
+ synchronized (mListeners) {
+ String tag = getClass().getSimpleName() + "#setIsDreaming";
+ DejankUtils.startDetectingBlockingIpcs(tag);
+ for (RankedListener rl : new ArrayList<>(mListeners)) {
+ rl.mListener.onDreamingChanged(isDreaming);
+ }
+ DejankUtils.stopDetectingBlockingIpcs(tag);
+ }
+
+ return true;
+ }
+
+ @Override
public void setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated) {
if (mDarkAnimator != null && mDarkAnimator.isRunning()) {
if (animated && mDozeAmountTarget == dozeAmount) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index e0cf812..088c568 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -99,6 +99,13 @@
boolean setIsDozing(boolean isDozing);
/**
+ * Update the dreaming state from {@link CentralSurfaces}'s perspective
+ * @param isDreaming whether we are dreaming
+ * @return {@code true} if the state changed, else {@code false}
+ */
+ boolean setIsDreaming(boolean isDreaming);
+
+ /**
* Changes the current doze amount, also starts the
* {@link com.android.internal.jank.InteractionJankMonitor InteractionJankMonitor} as possible.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
index f960eb7..ed32008 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkController.java
@@ -47,8 +47,6 @@
void addEmergencyListener(EmergencyListener listener);
/** */
void removeEmergencyListener(EmergencyListener listener);
- /** */
- boolean hasEmergencyCryptKeeperText();
/** */
boolean isRadioOn();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 5f5418f..6e74542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -84,7 +84,6 @@
import com.android.systemui.statusbar.policy.DataSaverControllerImpl;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
-import com.android.systemui.statusbar.policy.EncryptionHelper;
import com.android.systemui.telephony.TelephonyListenerManager;
import com.android.systemui.util.CarrierConfigTracker;
@@ -1486,11 +1485,6 @@
}
/** */
- public boolean hasEmergencyCryptKeeperText() {
- return EncryptionHelper.IS_DATA_ENCRYPTED;
- }
-
- /** */
public boolean isRadioOn() {
return !mAirplaneMode;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 14d0d7e..d7568a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -25,16 +25,21 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.AnimationFeatureFlags;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.carrier.QSCarrierGroupController;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.ActionClickLogger;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.MediaArtworkProcessor;
@@ -61,7 +66,6 @@
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarIconList;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags;
@@ -181,11 +185,12 @@
@SysUISingleton
static CommandQueue provideCommandQueue(
Context context,
+ DisplayTracker displayTracker,
ProtoTracer protoTracer,
CommandRegistry registry,
DumpHandler dumpHandler
) {
- return new CommandQueue(context, protoTracer, registry, dumpHandler);
+ return new CommandQueue(context, displayTracker, protoTracer, registry, dumpHandler);
}
/**
@@ -280,8 +285,9 @@
@SysUISingleton
static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager,
KeyguardStateController keyguardStateController,
- Lazy<StatusBarKeyguardViewManager> statusBarKeyguardViewManager,
- InteractionJankMonitor interactionJankMonitor) {
+ Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
+ InteractionJankMonitor interactionJankMonitor,
+ AnimationFeatureFlags animationFeatureFlags) {
DialogLaunchAnimator.Callback callback = new DialogLaunchAnimator.Callback() {
@Override
public boolean isDreaming() {
@@ -300,9 +306,22 @@
@Override
public boolean isShowingAlternateAuthOnUnlock() {
- return statusBarKeyguardViewManager.get().canShowAlternateBouncer();
+ return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint();
}
};
- return new DialogLaunchAnimator(callback, interactionJankMonitor);
+ return new DialogLaunchAnimator(callback, interactionJankMonitor, animationFeatureFlags);
+ }
+
+ /**
+ */
+ @Provides
+ @SysUISingleton
+ static AnimationFeatureFlags provideAnimationFeatureFlags(FeatureFlags featureFlags) {
+ return new AnimationFeatureFlags() {
+ @Override
+ public boolean isPredictiveBackQsDialogAnim() {
+ return featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM);
+ }
+ };
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 1e7fc93..197cf56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -54,7 +54,7 @@
* their respective views based on the progress of the animator. Interpolation differences TBD
*/
@SysUISingleton
-class SystemStatusAnimationScheduler @Inject constructor(
+open class SystemStatusAnimationScheduler @Inject constructor(
private val coordinator: SystemEventCoordinator,
private val chipAnimationController: SystemEventChipAnimationController,
private val statusBarWindowController: StatusBarWindowController,
@@ -66,7 +66,7 @@
companion object {
private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
}
- private fun isImmersiveIndicatorEnabled(): Boolean {
+ public fun isImmersiveIndicatorEnabled(): Boolean {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true)
}
@@ -76,18 +76,22 @@
/** True if the persistent privacy dot should be active */
var hasPersistentDot = false
- private set
+ protected set
private var scheduledEvent: StatusEvent? = null
private var cancelExecutionRunnable: Runnable? = null
private val listeners = mutableSetOf<SystemStatusAnimationCallback>()
+ fun getListeners(): MutableSet<SystemStatusAnimationCallback> {
+ return listeners
+ }
+
init {
coordinator.attachScheduler(this)
dumpManager.registerDumpable(TAG, this)
}
- fun onStatusEvent(event: StatusEvent) {
+ open fun onStatusEvent(event: StatusEvent) {
// Ignore any updates until the system is up and running
if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
return
@@ -139,7 +143,7 @@
}
}
- private fun isTooEarly(): Boolean {
+ public fun isTooEarly(): Boolean {
return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
index 3a4731a..92a8356 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt
@@ -20,9 +20,9 @@
import android.annotation.CallSuper
import android.os.Looper
import android.view.Choreographer
-import android.view.Display
import android.view.InputEvent
import android.view.MotionEvent
+import com.android.systemui.settings.DisplayTracker
import com.android.systemui.shared.system.InputChannelCompat
import com.android.systemui.shared.system.InputMonitorCompat
@@ -38,7 +38,8 @@
* gesture is detected, they should call [onGestureDetected] (which will notify the callbacks).
*/
abstract class GenericGestureDetector(
- private val tag: String
+ private val tag: String,
+ private val displayTracker: DisplayTracker
) {
/**
* Active callbacks, each associated with a tag. Gestures will only be monitored if
@@ -86,7 +87,7 @@
internal open fun startGestureListening() {
stopGestureListening()
- inputMonitor = InputMonitorCompat(tag, Display.DEFAULT_DISPLAY).also {
+ inputMonitor = InputMonitorCompat(tag, displayTracker.defaultDisplayId).also {
inputReceiver = it.getInputReceiver(
Looper.getMainLooper(),
Choreographer.getInstance(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
index 6115819..693ae66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
@@ -17,90 +17,27 @@
package com.android.systemui.statusbar.gesture
import android.content.Context
-import android.view.InputEvent
import android.view.MotionEvent
-import android.view.MotionEvent.ACTION_CANCEL
-import android.view.MotionEvent.ACTION_DOWN
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_UP
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
import com.android.systemui.statusbar.window.StatusBarWindowController
import javax.inject.Inject
-/**
- * A class to detect when a user swipes away the status bar. To be notified when the swipe away
- * gesture is detected, add a callback via [addOnGestureDetectedCallback].
- */
+/** A class to detect when a user swipes away the status bar. */
@SysUISingleton
-open class SwipeStatusBarAwayGestureHandler @Inject constructor(
+class SwipeStatusBarAwayGestureHandler
+@Inject
+constructor(
context: Context,
+ displayTracker: DisplayTracker,
+ logger: SwipeUpGestureLogger,
private val statusBarWindowController: StatusBarWindowController,
- private val logger: SwipeStatusBarAwayGestureLogger
-) : GenericGestureDetector(SwipeStatusBarAwayGestureHandler::class.simpleName!!) {
-
- private var startY: Float = 0f
- private var startTime: Long = 0L
- private var monitoringCurrentTouch: Boolean = false
-
- private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
- com.android.internal.R.dimen.system_gestures_start_threshold
- )
-
- override fun onInputEvent(ev: InputEvent) {
- if (ev !is MotionEvent) {
- return
- }
-
- when (ev.actionMasked) {
- ACTION_DOWN -> {
- if (
- // Gesture starts just below the status bar
- ev.y >= statusBarWindowController.statusBarHeight
- && ev.y <= 3 * statusBarWindowController.statusBarHeight
- ) {
- logger.logGestureDetectionStarted(ev.y.toInt())
- startY = ev.y
- startTime = ev.eventTime
- monitoringCurrentTouch = true
- } else {
- monitoringCurrentTouch = false
- }
- }
- ACTION_MOVE -> {
- if (!monitoringCurrentTouch) {
- return
- }
- if (
- // Gesture is up
- ev.y < startY
- // Gesture went far enough
- && (startY - ev.y) >= swipeDistanceThreshold
- // Gesture completed quickly enough
- && (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
- ) {
- monitoringCurrentTouch = false
- logger.logGestureDetected(ev.y.toInt())
- onGestureDetected(ev)
- }
- }
- ACTION_CANCEL, ACTION_UP -> {
- if (monitoringCurrentTouch) {
- logger.logGestureDetectionEndedWithoutTriggering(ev.y.toInt())
- }
- monitoringCurrentTouch = false
- }
- }
- }
-
- override fun startGestureListening() {
- super.startGestureListening()
- logger.logInputListeningStarted()
- }
-
- override fun stopGestureListening() {
- super.stopGestureListening()
- logger.logInputListeningStopped()
+) : SwipeUpGestureHandler(context, displayTracker, logger, loggerTag = LOGGER_TAG) {
+ override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
+ // Gesture starts just below the status bar
+ return ev.y >= statusBarWindowController.statusBarHeight &&
+ ev.y <= 3 * statusBarWindowController.statusBarHeight
}
}
-private const val SWIPE_TIMEOUT_MS: Long = 500
+private const val LOGGER_TAG = "SwipeStatusBarAway"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
new file mode 100644
index 0000000..452762d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.statusbar.gesture
+
+import android.content.Context
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
+
+/**
+ * A class to detect a generic "swipe up" gesture. To be notified when the swipe up gesture is
+ * detected, add a callback via [addOnGestureDetectedCallback].
+ */
+@SysUISingleton
+abstract class SwipeUpGestureHandler(
+ context: Context,
+ displayTracker: DisplayTracker,
+ private val logger: SwipeUpGestureLogger,
+ private val loggerTag: String
+) : GenericGestureDetector(SwipeUpGestureHandler::class.simpleName!!, displayTracker) {
+
+ private var startY: Float = 0f
+ private var startTime: Long = 0L
+ private var monitoringCurrentTouch: Boolean = false
+
+ private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.system_gestures_start_threshold
+ )
+
+ override fun onInputEvent(ev: InputEvent) {
+ if (ev !is MotionEvent) {
+ return
+ }
+
+ when (ev.actionMasked) {
+ ACTION_DOWN -> {
+ if (
+ startOfGestureIsWithinBounds(ev)
+ ) {
+ logger.logGestureDetectionStarted(loggerTag, ev.y.toInt())
+ startY = ev.y
+ startTime = ev.eventTime
+ monitoringCurrentTouch = true
+ } else {
+ monitoringCurrentTouch = false
+ }
+ }
+ ACTION_MOVE -> {
+ if (!monitoringCurrentTouch) {
+ return
+ }
+ if (
+ // Gesture is up
+ ev.y < startY &&
+ // Gesture went far enough
+ (startY - ev.y) >= swipeDistanceThreshold &&
+ // Gesture completed quickly enough
+ (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
+ ) {
+ monitoringCurrentTouch = false
+ logger.logGestureDetected(loggerTag, ev.y.toInt())
+ onGestureDetected(ev)
+ }
+ }
+ ACTION_CANCEL, ACTION_UP -> {
+ if (monitoringCurrentTouch) {
+ logger.logGestureDetectionEndedWithoutTriggering(loggerTag, ev.y.toInt())
+ }
+ monitoringCurrentTouch = false
+ }
+ }
+ }
+
+ /**
+ * Returns true if the [ACTION_DOWN] event falls within bounds for this specific swipe-up
+ * gesture.
+ *
+ * Implementations must override this method to specify what part(s) of the screen are valid
+ * locations for the swipe up gesture to start at.
+ */
+ abstract fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean
+
+ override fun startGestureListening() {
+ super.startGestureListening()
+ logger.logInputListeningStarted(loggerTag)
+ }
+
+ override fun stopGestureListening() {
+ super.stopGestureListening()
+ logger.logInputListeningStopped(loggerTag)
+ }
+}
+
+private const val SWIPE_TIMEOUT_MS: Long = 500
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
index 9bdff92..9ce6b02 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt
@@ -16,49 +16,49 @@
package com.android.systemui.statusbar.gesture
-import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.SwipeUpLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject
-/** Log messages for [SwipeStatusBarAwayGestureHandler]. */
-class SwipeStatusBarAwayGestureLogger @Inject constructor(
- @SwipeStatusBarAwayLog private val buffer: LogBuffer
+/** Log messages for [SwipeUpGestureHandler]. */
+@SysUISingleton
+class SwipeUpGestureLogger @Inject constructor(
+ @SwipeUpLog private val buffer: LogBuffer,
) {
- fun logGestureDetectionStarted(y: Int) {
+ fun logGestureDetectionStarted(tag: String, y: Int) {
buffer.log(
- TAG,
+ tag,
LogLevel.DEBUG,
{ int1 = y },
{ "Beginning gesture detection. y=$int1" }
)
}
- fun logGestureDetectionEndedWithoutTriggering(y: Int) {
+ fun logGestureDetectionEndedWithoutTriggering(tag: String, y: Int) {
buffer.log(
- TAG,
+ tag,
LogLevel.DEBUG,
{ int1 = y },
{ "Gesture finished; no swipe up gesture detected. Final y=$int1" }
)
}
- fun logGestureDetected(y: Int) {
+ fun logGestureDetected(tag: String, y: Int) {
buffer.log(
- TAG,
+ tag,
LogLevel.INFO,
{ int1 = y },
{ "Gesture detected; notifying callbacks. y=$int1" }
)
}
- fun logInputListeningStarted() {
- buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening started "})
+ fun logInputListeningStarted(tag: String) {
+ buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening started "})
}
- fun logInputListeningStopped() {
- buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening stopped "})
+ fun logInputListeningStopped(tag: String) {
+ buffer.log(tag, LogLevel.VERBOSE, {}, { "Input listening stopped "})
}
}
-
-private const val TAG = "SwipeStatusBarAwayGestureHandler"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
index 7ffb07a..a901d597 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt
@@ -21,6 +21,7 @@
import android.view.InputEvent
import android.view.MotionEvent
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.DisplayTracker
import javax.inject.Inject
/**
@@ -29,8 +30,9 @@
*/
@SysUISingleton
class TapGestureDetector @Inject constructor(
- private val context: Context
-) : GenericGestureDetector(TapGestureDetector::class.simpleName!!) {
+ private val context: Context,
+ displayTracker: DisplayTracker
+) : GenericGestureDetector(TapGestureDetector::class.simpleName!!, displayTracker) {
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index d6ad7d0..6a9761d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -43,6 +43,7 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
@@ -51,6 +52,8 @@
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.regionsampling.RegionSampler
import com.android.systemui.shared.regionsampling.UpdateColorCallback
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN
+import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -59,12 +62,13 @@
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
+import javax.inject.Named
-/**
- * Controller for managing the smartspace view on the lockscreen
- */
+/** Controller for managing the smartspace view on the lockscreen */
@SysUISingleton
-class LockscreenSmartspaceController @Inject constructor(
+class LockscreenSmartspaceController
+@Inject
+constructor(
private val context: Context,
private val featureFlags: FeatureFlags,
private val smartspaceManager: SmartspaceManager,
@@ -81,14 +85,22 @@
@Main private val uiExecutor: Executor,
@Background private val bgExecutor: Executor,
@Main private val handler: Handler,
- optionalPlugin: Optional<BcSmartspaceDataPlugin>
+ @Named(DATE_SMARTSPACE_DATA_PLUGIN)
+ optionalDatePlugin: Optional<BcSmartspaceDataPlugin>,
+ @Named(WEATHER_SMARTSPACE_DATA_PLUGIN)
+ optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
+ optionalPlugin: Optional<BcSmartspaceDataPlugin>,
+ optionalConfigPlugin: Optional<BcSmartspaceConfigPlugin>,
) {
companion object {
private const val TAG = "LockscreenSmartspaceController"
}
private var session: SmartspaceSession? = null
+ private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
+ private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
+ private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null)
// Smartspace can be used on multiple displays, such as when the user casts their screen
private var smartspaceViews = mutableSetOf<SmartspaceView>()
@@ -97,7 +109,7 @@
private val regionSamplingEnabled =
featureFlags.isEnabled(Flags.REGION_SAMPLING)
-
+ private var isContentUpdatedOnce = false
private var showNotifications = false
private var showSensitiveContentForCurrentUser = false
private var showSensitiveContentForManagedUser = false
@@ -111,19 +123,6 @@
override fun onViewAttachedToWindow(v: View) {
smartspaceViews.add(v as SmartspaceView)
- if (regionSamplingEnabled) {
- var regionSampler = RegionSampler(
- v,
- uiExecutor,
- bgExecutor,
- regionSamplingEnabled,
- updateFun
- )
- initializeTextColors(regionSampler)
- regionSampler.startRegionSampler()
- regionSamplers.put(v, regionSampler)
- }
-
connectSession()
updateTextColorFromWallpaper()
@@ -133,12 +132,6 @@
override fun onViewDetachedFromWindow(v: View) {
smartspaceViews.remove(v as SmartspaceView)
- if (regionSamplingEnabled) {
- var regionSampler = regionSamplers.getValue(v)
- regionSampler.stopRegionSampler()
- regionSamplers.remove(v)
- }
-
if (smartspaceViews.isEmpty()) {
disconnect()
}
@@ -147,8 +140,30 @@
private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets ->
execution.assertIsMainThread()
+
+ // The weather data plugin takes unfiltered targets and performs the filtering internally.
+ weatherPlugin?.onTargetsAvailable(targets)
+
val filteredTargets = targets.filter(::filterSmartspaceTarget)
plugin?.onTargetsAvailable(filteredTargets)
+ if (!isContentUpdatedOnce) {
+ for (v in smartspaceViews) {
+ if (regionSamplingEnabled) {
+ var regionSampler = RegionSampler(
+ v as View,
+ uiExecutor,
+ bgExecutor,
+ regionSamplingEnabled,
+ updateFun
+ )
+ initializeTextColors(regionSampler)
+ regionSamplers[v] = regionSampler
+ regionSampler.startRegionSampler()
+ }
+ updateTextColorFromWallpaper()
+ }
+ isContentUpdatedOnce = true
+ }
}
private val userTrackerCallback = object : UserTracker.Callback {
@@ -204,7 +219,14 @@
fun isEnabled(): Boolean {
execution.assertIsMainThread()
- return featureFlags.isEnabled(Flags.SMARTSPACE) && plugin != null
+ return plugin != null
+ }
+
+ fun isDateWeatherDecoupled(): Boolean {
+ execution.assertIsMainThread()
+
+ return featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED) &&
+ datePlugin != null && weatherPlugin != null
}
private fun updateBypassEnabled() {
@@ -213,6 +235,44 @@
}
/**
+ * Constructs the date view and connects it to the smartspace service.
+ */
+ fun buildAndConnectDateView(parent: ViewGroup): View? {
+ execution.assertIsMainThread()
+
+ if (!isEnabled()) {
+ throw RuntimeException("Cannot build view when not enabled")
+ }
+ if (!isDateWeatherDecoupled()) {
+ throw RuntimeException("Cannot build date view when not decoupled")
+ }
+
+ val view = buildView(parent, datePlugin)
+ connectSession()
+
+ return view
+ }
+
+ /**
+ * Constructs the weather view and connects it to the smartspace service.
+ */
+ fun buildAndConnectWeatherView(parent: ViewGroup): View? {
+ execution.assertIsMainThread()
+
+ if (!isEnabled()) {
+ throw RuntimeException("Cannot build view when not enabled")
+ }
+ if (!isDateWeatherDecoupled()) {
+ throw RuntimeException("Cannot build weather view when not decoupled")
+ }
+
+ val view = buildView(parent, weatherPlugin)
+ connectSession()
+
+ return view
+ }
+
+ /**
* Constructs the smartspace view and connects it to the smartspace service.
*/
fun buildAndConnectView(parent: ViewGroup): View? {
@@ -222,17 +282,17 @@
throw RuntimeException("Cannot build view when not enabled")
}
- val view = buildView(parent)
+ val view = buildView(parent, plugin, configPlugin)
connectSession()
return view
}
- fun requestSmartspaceUpdate() {
- session?.requestSmartspaceUpdate()
- }
-
- private fun buildView(parent: ViewGroup): View? {
+ private fun buildView(
+ parent: ViewGroup,
+ plugin: BcSmartspaceDataPlugin?,
+ configPlugin: BcSmartspaceConfigPlugin? = null
+ ): View? {
if (plugin == null) {
return null
}
@@ -240,6 +300,7 @@
val ssView = plugin.getView(parent)
ssView.setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
ssView.registerDataProvider(plugin)
+ configPlugin?.let { ssView.registerConfigProvider(it) }
ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) {
@@ -270,7 +331,8 @@
}
private fun connectSession() {
- if (plugin == null || session != null || smartspaceViews.isEmpty()) {
+ if (datePlugin == null && weatherPlugin == null && plugin == null) return
+ if (session != null || smartspaceViews.isEmpty()) {
return
}
@@ -307,15 +369,22 @@
statusBarStateController.addCallback(statusBarStateListener)
bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
- plugin.registerSmartspaceEventNotifier {
- e -> session?.notifySmartspaceEvent(e)
- }
+ datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
+ weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
+ plugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
updateBypassEnabled()
reloadSmartspace()
}
/**
+ * Requests the smartspace session for an update.
+ */
+ fun requestSmartspaceUpdate() {
+ session?.requestSmartspaceUpdate()
+ }
+
+ /**
* Disconnects the smartspace view from the smartspace service and cleans up any resources.
*/
fun disconnect() {
@@ -338,9 +407,15 @@
bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
session = null
+ datePlugin?.registerSmartspaceEventNotifier(null)
+
+ weatherPlugin?.registerSmartspaceEventNotifier(null)
+ weatherPlugin?.onTargetsAvailable(emptyList())
+
plugin?.registerSmartspaceEventNotifier(null)
plugin?.onTargetsAvailable(emptyList())
- Log.d(TAG, "Ending smartspace session for lockscreen")
+
+ Log.d(TAG, "Ended smartspace session for lockscreen")
}
fun addListener(listener: SmartspaceTargetListener) {
@@ -354,8 +429,11 @@
}
private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean {
+ if (isDateWeatherDecoupled()) {
+ return t.featureType != SmartspaceTarget.FEATURE_WEATHER
+ }
if (!showNotifications) {
- return t.getFeatureType() == SmartspaceTarget.FEATURE_WEATHER
+ return t.featureType == SmartspaceTarget.FEATURE_WEATHER
}
return when (t.userHandle) {
userTracker.userHandle -> {
@@ -394,7 +472,8 @@
private fun updateTextColorFromWallpaper() {
val wallpaperManager = WallpaperManager.getInstance(context)
- if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists()) {
+ if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists() ||
+ regionSamplers.isEmpty()) {
val wallpaperTextColor =
Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)
smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 3072c81..4856759 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -25,23 +25,19 @@
val context: Context,
val featureFlags: FeatureFlags
) {
+ init {
+ featureFlags.addListener(Flags.DISABLE_FSI) { event -> event.requestNoRestart() }
+ }
+
fun isDevLoggingEnabled(): Boolean =
featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
- fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE)
-
fun fullScreenIntentRequiresKeyguard(): Boolean =
featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE)
- val isStabilityIndexFixEnabled: Boolean by lazy {
- featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
- }
-
- val isSemiStableSortEnabled: Boolean by lazy {
- featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
- }
+ fun disableFsi(): Boolean = featureFlags.isEnabled(Flags.DISABLE_FSI)
val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy {
featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index aeae89c..7e53d54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -31,6 +31,7 @@
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.KeyguardBypassController.OnBypassStateChangedListener
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
@@ -38,7 +39,6 @@
import javax.inject.Inject
import kotlin.math.min
-
@SysUISingleton
class NotificationWakeUpCoordinator @Inject constructor(
dumpManager: DumpManager,
@@ -68,6 +68,7 @@
private var mLinearDozeAmount: Float = 0.0f
private var mDozeAmount: Float = 0.0f
private var mDozeAmountSource: String = "init"
+ private var mNotifsHiddenByDozeAmountOverride: Boolean = false
private var mNotificationVisibleAmount = 0.0f
private var mNotificationsVisible = false
private var mNotificationsVisibleForExpansion = false
@@ -130,6 +131,7 @@
}
}
}
+
/**
* True if we can show pulsing heads up notifications
*/
@@ -149,10 +151,19 @@
return canShow
}
+ private val bypassStateChangedListener = object : OnBypassStateChangedListener {
+ override fun onBypassStateChanged(isEnabled: Boolean) {
+ // When the bypass state changes, we have to check whether we should re-show the
+ // notifications by clearing the doze amount override which hides them.
+ maybeClearDozeAmountOverrideHidingNotifs()
+ }
+ }
+
init {
dumpManager.registerDumpable(this)
mHeadsUpManager.addListener(this)
statusBarStateController.addCallback(this)
+ bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
addListener(object : WakeUpListener {
override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
if (isFullyHidden && mNotificationsVisibleForExpansion) {
@@ -261,12 +272,18 @@
setDozeAmount(linear, eased, source = "StatusBar")
}
- fun setDozeAmount(linear: Float, eased: Float, source: String) {
+ fun setDozeAmount(
+ linear: Float,
+ eased: Float,
+ source: String,
+ hidesNotifsByOverride: Boolean = false
+ ) {
val changed = linear != mLinearDozeAmount
logger.logSetDozeAmount(linear, eased, source, statusBarStateController.state, changed)
mLinearDozeAmount = linear
mDozeAmount = eased
mDozeAmountSource = source
+ mNotifsHiddenByDozeAmountOverride = hidesNotifsByOverride
mStackScrollerController.setDozeAmount(mDozeAmount)
updateHideAmount()
if (changed && linear == 0.0f) {
@@ -295,6 +312,8 @@
return
}
+ maybeClearDozeAmountOverrideHidingNotifs()
+
if (bypassController.bypassEnabled &&
newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED &&
(!statusBarStateController.isDozing || shouldAnimateVisibility())) {
@@ -325,7 +344,8 @@
private fun overrideDozeAmountIfBypass(): Boolean {
if (bypassController.bypassEnabled) {
if (statusBarStateController.state == StatusBarState.KEYGUARD) {
- setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
+ setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)",
+ hidesNotifsByOverride = true)
} else {
setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
}
@@ -335,6 +355,37 @@
}
/**
+ * If the last [setDozeAmount] call was an override to hide notifications, then this call will
+ * check for the set of states that may have caused that override, and if none of them still
+ * apply, and the device is awake or not on the keyguard, then dozeAmount will be reset to 0.
+ * This fixes bugs where the bypass state changing could result in stale overrides, hiding
+ * notifications either on the inside screen or even after unlock.
+ */
+ private fun maybeClearDozeAmountOverrideHidingNotifs() {
+ if (mNotifsHiddenByDozeAmountOverride) {
+ val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
+ val dozing = statusBarStateController.isDozing
+ val bypass = bypassController.bypassEnabled
+ val animating =
+ screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()
+ // Overrides are set by [overrideDozeAmountIfAnimatingScreenOff] and
+ // [overrideDozeAmountIfBypass] based on 'animating' and 'bypass' respectively, so only
+ // clear the override if both those conditions are cleared. But also require either
+ // !dozing or !onKeyguard because those conditions should indicate that we intend
+ // notifications to be visible, and thus it is safe to unhide them.
+ val willRemove = (!onKeyguard || !dozing) && !bypass && !animating
+ logger.logMaybeClearDozeAmountOverrideHidingNotifs(
+ willRemove = willRemove,
+ onKeyguard = onKeyguard, dozing = dozing,
+ bypass = bypass, animating = animating,
+ )
+ if (willRemove) {
+ setDozeAmount(0f, 0f, source = "Removed: $mDozeAmountSource")
+ }
+ }
+ }
+
+ /**
* If we're playing the screen off animation, force the notification doze amount to be 1f (fully
* dozing). This is needed so that the notifications aren't briefly visible as the screen turns
* off and dozeAmount goes from 1f to 0f.
@@ -344,7 +395,8 @@
*/
private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean {
if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) {
- setDozeAmount(1f, 1f, source = "Override: animating screen off")
+ setDozeAmount(1f, 1f, source = "Override: animating screen off",
+ hidesNotifsByOverride = true)
return true
}
@@ -430,6 +482,7 @@
pw.println("mLinearDozeAmount: $mLinearDozeAmount")
pw.println("mDozeAmount: $mDozeAmount")
pw.println("mDozeAmountSource: $mDozeAmountSource")
+ pw.println("mNotifsHiddenByDozeAmountOverride: $mNotifsHiddenByDozeAmountOverride")
pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
pw.println("mNotificationsVisible: $mNotificationsVisible")
pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
index de18b0c..4464531 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
@@ -46,6 +46,25 @@
)
}
+ fun logMaybeClearDozeAmountOverrideHidingNotifs(
+ willRemove: Boolean,
+ onKeyguard: Boolean,
+ dozing: Boolean,
+ bypass: Boolean,
+ animating: Boolean,
+ ) {
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ str1 =
+ "willRemove=$willRemove onKeyguard=$onKeyguard dozing=$dozing" +
+ " bypass=$bypass animating=$animating"
+ },
+ { "maybeClearDozeAmountOverrideHidingNotifs() $str1" }
+ )
+ }
+
fun logOnDozeAmountChanged(linear: Float, eased: Float) {
buffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index 84ab0d1..b5fce41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -98,13 +98,11 @@
* This can happen if the entry is removed from a group that was broken up or if the entry was
* filtered out during any of the filtering steps.
*/
- fun detach(includingStableIndex: Boolean) {
+ fun detach() {
parent = null
section = null
promoter = null
- if (includingStableIndex) {
- stableIndex = -1
- }
+ stableIndex = -1
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 65a21a4..4065b98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -965,8 +965,7 @@
* filtered out during any of the filtering steps.
*/
private void annulAddition(ListEntry entry) {
- // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
- entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
+ entry.getAttachState().detach();
}
private void assignSections() {
@@ -986,50 +985,10 @@
private void sortListAndGroups() {
Trace.beginSection("ShadeListBuilder.sortListAndGroups");
- if (mFlags.isSemiStableSortEnabled()) {
- sortWithSemiStableSort();
- } else {
- sortWithLegacyStability();
- }
+ sortWithSemiStableSort();
Trace.endSection();
}
- private void sortWithLegacyStability() {
- // Sort all groups and the top level list
- for (ListEntry entry : mNotifList) {
- if (entry instanceof GroupEntry) {
- GroupEntry parent = (GroupEntry) entry;
- parent.sortChildren(mGroupChildrenComparator);
- }
- }
- mNotifList.sort(mTopLevelComparator);
- assignIndexes(mNotifList);
-
- // Check for suppressed order changes
- if (!getStabilityManager().isEveryChangeAllowed()) {
- mForceReorderable = true;
- boolean isSorted = isShadeSortedLegacy();
- mForceReorderable = false;
- if (!isSorted) {
- getStabilityManager().onEntryReorderSuppressed();
- }
- }
- }
-
- private boolean isShadeSortedLegacy() {
- if (!isSorted(mNotifList, mTopLevelComparator)) {
- return false;
- }
- for (ListEntry entry : mNotifList) {
- if (entry instanceof GroupEntry) {
- if (!isSorted(((GroupEntry) entry).getChildren(), mGroupChildrenComparator)) {
- return false;
- }
- }
- }
- return true;
- }
-
private void sortWithSemiStableSort() {
// Sort each group's children
boolean allSorted = true;
@@ -1100,29 +1059,16 @@
sectionMemberIndex = 0;
currentSection = section;
}
- if (mFlags.isStabilityIndexFixEnabled()) {
- entry.getAttachState().setStableIndex(sectionMemberIndex++);
- if (entry instanceof GroupEntry) {
- final GroupEntry parent = (GroupEntry) entry;
- final NotificationEntry summary = parent.getSummary();
- if (summary != null) {
- summary.getAttachState().setStableIndex(sectionMemberIndex++);
- }
- for (NotificationEntry child : parent.getChildren()) {
- child.getAttachState().setStableIndex(sectionMemberIndex++);
- }
+ entry.getAttachState().setStableIndex(sectionMemberIndex++);
+ if (entry instanceof GroupEntry) {
+ final GroupEntry parent = (GroupEntry) entry;
+ final NotificationEntry summary = parent.getSummary();
+ if (summary != null) {
+ summary.getAttachState().setStableIndex(sectionMemberIndex++);
}
- } else {
- // This old implementation uses the same index number for the group as the first
- // child, and fails to assign an index to the summary. Remove once tested.
- entry.getAttachState().setStableIndex(sectionMemberIndex);
- if (entry instanceof GroupEntry) {
- final GroupEntry parent = (GroupEntry) entry;
- for (NotificationEntry child : parent.getChildren()) {
- child.getAttachState().setStableIndex(sectionMemberIndex++);
- }
+ for (NotificationEntry child : parent.getChildren()) {
+ child.getAttachState().setStableIndex(sectionMemberIndex++);
}
- sectionMemberIndex++;
}
}
}
@@ -1272,11 +1218,6 @@
o2.getSectionIndex());
if (cmp != 0) return cmp;
- cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
- getStableOrderIndex(o1),
- getStableOrderIndex(o2));
- if (cmp != 0) return cmp;
-
NotifComparator sectionComparator = getSectionComparator(o1, o2);
if (sectionComparator != null) {
cmp = sectionComparator.compare(o1, o2);
@@ -1301,12 +1242,7 @@
private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
- int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
- getStableOrderIndex(o1),
- getStableOrderIndex(o2));
- if (cmp != 0) return cmp;
-
- cmp = Integer.compare(
+ int cmp = Integer.compare(
o1.getRepresentativeEntry().getRanking().getRank(),
o2.getRepresentativeEntry().getRanking().getRank());
if (cmp != 0) return cmp;
@@ -1317,25 +1253,6 @@
return cmp;
};
- /**
- * A flag that is set to true when we want to run the comparators as if all reordering is
- * allowed. This is used to check if the list is "out of order" after the sort is complete.
- */
- private boolean mForceReorderable = false;
-
- private int getStableOrderIndex(ListEntry entry) {
- if (mForceReorderable) {
- // this is used to determine if the list is correctly sorted
- return -1;
- }
- if (getStabilityManager().isEntryReorderingAllowed(entry)) {
- // let the stability manager constrain or allow reordering
- return -1;
- }
- // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
- return entry.getPreviousAttachState().getStableIndex();
- }
-
@Nullable
private Integer getStableOrderRank(ListEntry entry) {
if (getStabilityManager().isEntryReorderingAllowed(entry)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 76252d0..e996b78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -114,10 +114,10 @@
.onStart { emit(Unit) }
// for each change, lookup the new value
.map {
- secureSettings.getBoolForUser(
+ secureSettings.getIntForUser(
Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
UserHandle.USER_CURRENT,
- )
+ ) == 1
}
// perform lookups on the bg thread pool
.flowOn(bgDispatcher)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 1399385..03a3ca5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -88,9 +88,7 @@
mCoordinators.add(viewConfigCoordinator)
mCoordinators.add(visualStabilityCoordinator)
mCoordinators.add(sensitiveContentCoordinator)
- if (notifPipelineFlags.isSmartspaceDedupingEnabled()) {
- mCoordinators.add(smartspaceDedupingCoordinator)
- }
+ mCoordinators.add(smartspaceDedupingCoordinator)
mCoordinators.add(headsUpCoordinator)
mCoordinators.add(gutsCoordinator)
mCoordinators.add(preparationCoordinator)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 808638a..8436ff7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -16,27 +16,15 @@
package com.android.systemui.statusbar.notification.dagger;
-import android.app.INotificationManager;
import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.content.pm.ShortcutManager;
-import android.os.Handler;
-import android.view.accessibility.AccessibilityManager;
-import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
-import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.settings.UserContextProvider;
-import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeEventsModule;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationListener;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
@@ -50,7 +38,6 @@
import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl;
import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderModule;
import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
@@ -72,22 +59,17 @@
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl;
-import com.android.systemui.statusbar.notification.row.ChannelEditorDialogController;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager;
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.wmshell.BubblesManager;
-import java.util.Optional;
import java.util.concurrent.Executor;
import javax.inject.Provider;
import dagger.Binds;
-import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
@@ -109,48 +91,6 @@
@Binds
StackScrollAlgorithm.BypassController bindBypassController(KeyguardBypassController impl);
- /** Provides an instance of {@link NotificationGutsManager} */
- @SysUISingleton
- @Provides
- static NotificationGutsManager provideNotificationGutsManager(
- Context context,
- Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
- @Main Handler mainHandler,
- @Background Handler bgHandler,
- AccessibilityManager accessibilityManager,
- HighPriorityProvider highPriorityProvider,
- INotificationManager notificationManager,
- PeopleSpaceWidgetManager peopleSpaceWidgetManager,
- LauncherApps launcherApps,
- ShortcutManager shortcutManager,
- ChannelEditorDialogController channelEditorDialogController,
- UserContextProvider contextTracker,
- AssistantFeedbackController assistantFeedbackController,
- Optional<BubblesManager> bubblesManagerOptional,
- UiEventLogger uiEventLogger,
- OnUserInteractionCallback onUserInteractionCallback,
- ShadeController shadeController) {
- return new NotificationGutsManager(
- context,
- centralSurfacesOptionalLazy,
- mainHandler,
- bgHandler,
- accessibilityManager,
- highPriorityProvider,
- notificationManager,
- peopleSpaceWidgetManager,
- launcherApps,
- shortcutManager,
- channelEditorDialogController,
- contextTracker,
- assistantFeedbackController,
- bubblesManagerOptional,
- uiEventLogger,
- onUserInteractionCallback,
- shadeController
- );
- }
-
/** Provides an instance of {@link NotifGutsViewManager} */
@Binds
NotifGutsViewManager bindNotifGutsViewManager(NotificationGutsManager notificationGutsManager);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
index 7136cad..bc881ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
@@ -31,6 +31,10 @@
*/
enum FullScreenIntentDecision {
/**
+ * Full screen intents are disabled.
+ */
+ NO_FSI_DISABLED(false),
+ /**
* No full screen intent included, so there is nothing to show.
*/
NO_FULL_SCREEN_INTENT(false),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index d9dacfd..9bcf92d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -28,7 +28,6 @@
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.provider.Settings;
import android.service.dreams.IDreamManager;
import android.service.notification.StatusBarNotification;
@@ -40,6 +39,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -74,6 +74,7 @@
private final NotifPipelineFlags mFlags;
private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
private final UiEventLogger mUiEventLogger;
+ private final UserTracker mUserTracker;
@VisibleForTesting
protected boolean mUseHeadsUp = false;
@@ -114,7 +115,8 @@
@Main Handler mainHandler,
NotifPipelineFlags flags,
KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
- UiEventLogger uiEventLogger) {
+ UiEventLogger uiEventLogger,
+ UserTracker userTracker) {
mContentResolver = contentResolver;
mPowerManager = powerManager;
mDreamManager = dreamManager;
@@ -127,6 +129,7 @@
mFlags = flags;
mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
mUiEventLogger = uiEventLogger;
+ mUserTracker = userTracker;
ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -236,6 +239,9 @@
@Override
public FullScreenIntentDecision getFullScreenIntentDecision(NotificationEntry entry) {
+ if (mFlags.disableFsi()) {
+ return FullScreenIntentDecision.NO_FSI_DISABLED;
+ }
if (entry.getSbn().getNotification().fullScreenIntent == null) {
return FullScreenIntentDecision.NO_FULL_SCREEN_INTENT;
}
@@ -325,6 +331,9 @@
final int uid = entry.getSbn().getUid();
final String packageName = entry.getSbn().getPackageName();
switch (decision) {
+ case NO_FSI_DISABLED:
+ mLogger.logNoFullscreen(entry, "Disabled");
+ return;
case NO_FULL_SCREEN_INTENT:
return;
case NO_FSI_SUPPRESSED_BY_DND:
@@ -450,7 +459,7 @@
* @return true if the entry should ambient pulse, false otherwise
*/
private boolean shouldHeadsUpWhenDozing(NotificationEntry entry, boolean log) {
- if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
+ if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(mUserTracker.getUserId())) {
if (log) mLogger.logNoPulsingSettingDisabled(entry);
return false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
index ffd931c..197ae1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt
@@ -18,9 +18,12 @@
package com.android.systemui.statusbar.notification.logging
import android.stats.sysui.NotificationEnums
+import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.dump.DumpsysTableLogger
+import com.android.systemui.dump.Row
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import java.io.PrintWriter
import javax.inject.Inject
@@ -33,6 +36,7 @@
fun init() {
dumpManager.registerNormalDumpable(javaClass.simpleName, this)
+ Log.i("NotificationMemory", "Registered dumpable.")
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -45,27 +49,36 @@
/** Renders a table of notification object usage into passed [PrintWriter]. */
private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) {
- pw.println("Notification Object Usage")
- pw.println("-----------")
- pw.println(
- "Package".padEnd(35) +
- "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom"
- )
- pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView")
- pw.println()
-
- memoryUse.forEach { use ->
- pw.println(
- use.packageName.padEnd(35) +
- "\t\t" +
- "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" +
- (styleEnumToString(use.objectUsage.style).take(15) ?: "").padEnd(15) +
- "\t\t${use.objectUsage.styleIcon}\t" +
- "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" +
- "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" +
- use.notificationKey
+ val columns =
+ listOf(
+ "Package",
+ "Small Icon",
+ "Large Icon",
+ "Style",
+ "Style Icon",
+ "Big Picture",
+ "Extender",
+ "Extras",
+ "Custom View",
+ "Key"
)
- }
+ val rows: List<Row> =
+ memoryUse.map {
+ listOf(
+ it.packageName,
+ toKb(it.objectUsage.smallIcon),
+ toKb(it.objectUsage.largeIcon),
+ styleEnumToString(it.objectUsage.style),
+ toKb(it.objectUsage.styleIcon),
+ toKb(it.objectUsage.bigPicture),
+ toKb(it.objectUsage.extender),
+ toKb(it.objectUsage.extras),
+ it.objectUsage.hasCustomView.toString(),
+ // | is a field delimiter in the output format so we need to replace
+ // it to avoid breakage.
+ it.notificationKey.replace('|', '│')
+ )
+ }
// Calculate totals for easily glanceable summary.
data class Totals(
@@ -88,18 +101,23 @@
t
}
- pw.println()
- pw.println("TOTALS")
- pw.println(
- "".padEnd(35) +
- "\t\t" +
- "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" +
- "".padEnd(15) +
- "\t\t${toKb(totals.styleIcon)}\t" +
- "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" +
- toKb(totals.extras)
- )
- pw.println()
+ val totalsRow: List<Row> =
+ listOf(
+ listOf(
+ "TOTALS",
+ toKb(totals.smallIcon),
+ toKb(totals.largeIcon),
+ "",
+ toKb(totals.styleIcon),
+ toKb(totals.bigPicture),
+ toKb(totals.extender),
+ toKb(totals.extras),
+ "",
+ ""
+ )
+ )
+ val tableLogger = DumpsysTableLogger("Notification Object Usage", columns, rows + totalsRow)
+ tableLogger.printTableData(pw)
}
/** Renders a table of notification view usage into passed [PrintWriter] */
@@ -116,40 +134,65 @@
var softwareBitmapsPenalty: Int = 0,
)
- val totals = Totals()
- pw.println("Notification View Usage")
- pw.println("-----------")
- pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware")
- pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps")
- pw.println()
- memoryUse
- .filter { it.viewUsage.isNotEmpty() }
- .forEach { use ->
- pw.println(use.packageName + " " + use.notificationKey)
- use.viewUsage.forEach { view ->
- pw.println(
- " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" +
- "\t${view.largeIcon}\t${view.style}" +
- "\t${view.customViews}\t${view.softwareBitmapsPenalty}"
- )
-
- if (view.viewType == ViewType.TOTAL) {
- totals.smallIcon += view.smallIcon
- totals.largeIcon += view.largeIcon
- totals.style += view.style
- totals.customViews += view.customViews
- totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
+ val columns =
+ listOf(
+ "Package",
+ "View Type",
+ "Small Icon",
+ "Large Icon",
+ "Style Use",
+ "Custom View",
+ "Software Bitmaps",
+ "Key"
+ )
+ val rows =
+ memoryUse
+ .filter { it.viewUsage.isNotEmpty() }
+ .flatMap { use ->
+ use.viewUsage.map { view ->
+ listOf(
+ use.packageName,
+ view.viewType.toString(),
+ toKb(view.smallIcon),
+ toKb(view.largeIcon),
+ toKb(view.style),
+ toKb(view.customViews),
+ toKb(view.softwareBitmapsPenalty),
+ // | is a field delimiter in the output format so we need to replace
+ // it to avoid breakage.
+ use.notificationKey.replace('|', '│')
+ )
}
}
+
+ val totals = Totals()
+ memoryUse
+ .filter { it.viewUsage.isNotEmpty() }
+ .map { it.viewUsage.firstOrNull { view -> view.viewType == ViewType.TOTAL } }
+ .filterNotNull()
+ .forEach { view ->
+ totals.smallIcon += view.smallIcon
+ totals.largeIcon += view.largeIcon
+ totals.style += view.style
+ totals.customViews += view.customViews
+ totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
}
- pw.println()
- pw.println("TOTALS")
- pw.println(
- " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" +
- "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" +
- "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}"
- )
- pw.println()
+
+ val totalsRow: List<Row> =
+ listOf(
+ listOf(
+ "TOTALS",
+ "",
+ toKb(totals.smallIcon),
+ toKb(totals.largeIcon),
+ toKb(totals.style),
+ toKb(totals.customViews),
+ toKb(totals.softwareBitmapsPenalty),
+ ""
+ )
+ )
+ val tableLogger = DumpsysTableLogger("Notification View Usage", columns, rows + totalsRow)
+ tableLogger.printTableData(pw)
}
private fun styleEnumToString(styleEnum: Int): String =
@@ -168,6 +211,10 @@
}
private fun toKb(bytes: Int): String {
- return (bytes / 1024).toString() + " KB"
+ if (bytes == 0) {
+ return "--"
+ }
+
+ return "%.2f KB".format(bytes / 1024f)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index fbe88df..7addc8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -24,6 +24,7 @@
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.MathUtils;
+import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
@@ -492,12 +493,9 @@
if (animationListener != null) {
mAppearAnimator.addListener(animationListener);
}
- if (delay > 0) {
- // we need to apply the initial state already to avoid drawn frames in the wrong state
- updateAppearAnimationAlpha();
- updateAppearRect();
- mAppearAnimator.setStartDelay(delay);
- }
+ // we need to apply the initial state already to avoid drawn frames in the wrong state
+ updateAppearAnimationAlpha();
+ updateAppearRect();
mAppearAnimator.addListener(new AnimatorListenerAdapter() {
private boolean mWasCancelled;
@@ -528,7 +526,20 @@
mWasCancelled = true;
}
});
- mAppearAnimator.start();
+
+ // Cache the original animator so we can check if the animation should be started in the
+ // Choreographer callback. It's possible that the original animator (mAppearAnimator) is
+ // replaced with a new value before the callback is called.
+ ValueAnimator cachedAnimator = mAppearAnimator;
+ // Even when delay=0, starting the animation on the next frame is necessary to avoid jank.
+ // Not doing so will increase the chances our Animator will be forced to skip a value of
+ // the animation's progression, causing stutter.
+ Choreographer.getInstance().postFrameCallbackDelayed(
+ frameTimeNanos -> {
+ if (mAppearAnimator == cachedAnimator) {
+ mAppearAnimator.start();
+ }
+ }, delay);
}
private int getCujType(boolean isAppearing) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java
index c496102..b084a76 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java
@@ -109,7 +109,7 @@
return true;
}
if (ev.getAction() == MotionEvent.ACTION_UP) {
- mView.setLastActionUpTime(SystemClock.uptimeMillis());
+ mView.setLastActionUpTime(ev.getEventTime());
}
// With a11y, just do nothing.
if (mAccessibilityManager.isTouchExplorationEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 9f50aef..a6b71dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -79,6 +79,8 @@
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -177,6 +179,7 @@
private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
private Optional<BubblesManager> mBubblesManagerOptional;
private MetricsLogger mMetricsLogger;
+ private FeatureFlags mFeatureFlags;
private int mIconTransformContentShift;
private int mMaxHeadsUpHeightBeforeN;
private int mMaxHeadsUpHeightBeforeP;
@@ -277,7 +280,7 @@
private boolean mChildIsExpanding;
private boolean mJustClicked;
- private boolean mIconAnimationRunning;
+ private boolean mAnimationRunning;
private boolean mShowNoBackground;
private ExpandableNotificationRow mNotificationParent;
private OnExpandClickListener mOnExpandClickListener;
@@ -451,10 +454,26 @@
return mPublicLayout;
}
- public void setIconAnimationRunning(boolean running) {
- for (NotificationContentView l : mLayouts) {
- setIconAnimationRunning(running, l);
+ /**
+ * Sets animations running in the layouts of this row, including public, private, and children.
+ * @param running whether the animations should be started running or stopped.
+ */
+ public void setAnimationRunning(boolean running) {
+ // Sets animations running in the private/public layouts.
+ if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE)) {
+ for (NotificationContentView l : mLayouts) {
+ if (l != null) {
+ l.setContentAnimationRunning(running);
+ setIconAnimationRunning(running, l);
+ }
+ }
+ } else {
+ for (NotificationContentView l : mLayouts) {
+ setIconAnimationRunning(running, l);
+ }
}
+ // For groups summaries with children, we want to set the children containers
+ // animating as well.
if (mIsSummaryWithChildren) {
NotificationViewWrapper viewWrapper = mChildrenContainer.getNotificationViewWrapper();
if (viewWrapper != null) {
@@ -468,12 +487,18 @@
mChildrenContainer.getAttachedChildren();
for (int i = 0; i < notificationChildren.size(); i++) {
ExpandableNotificationRow child = notificationChildren.get(i);
- child.setIconAnimationRunning(running);
+ child.setAnimationRunning(running);
}
}
- mIconAnimationRunning = running;
+ mAnimationRunning = running;
}
+ /**
+ * Starts or stops animations of the icons in all potential content views (regardless of
+ * whether they're contracted, expanded, etc).
+ *
+ * @param running whether to start or stop the icon's animation.
+ */
private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
if (layout != null) {
View contractedChild = layout.getContractedChild();
@@ -485,16 +510,29 @@
}
}
+ /**
+ * Starts or stops animations of the icon in the provided view's icon and right icon.
+ *
+ * @param running whether to start or stop the icon's animation.
+ * @param child the view with the icon to start or stop.
+ */
private void setIconAnimationRunningForChild(boolean running, View child) {
if (child != null) {
ImageView icon = child.findViewById(com.android.internal.R.id.icon);
- setIconRunning(icon, running);
+ setImageViewAnimationRunning(icon, running);
ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon);
- setIconRunning(rightIcon, running);
+ setImageViewAnimationRunning(rightIcon, running);
}
}
- private void setIconRunning(ImageView imageView, boolean running) {
+ /**
+ * Starts or stops the animation of a provided image view if it's an AnimationDrawable or an
+ * AnimatedVectorDrawable.
+ *
+ * @param imageView the image view on which to start/stop animation.
+ * @param running whether to start or stop the view's animation.
+ */
+ private void setImageViewAnimationRunning(ImageView imageView, boolean running) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof AnimationDrawable) {
@@ -553,6 +591,7 @@
}
mShowingPublicInitialized = false;
updateNotificationColor();
+ updateLongClickable();
if (mMenuRow != null) {
mMenuRow.onNotificationUpdated(mEntry.getSbn());
mMenuRow.setAppName(mAppName);
@@ -561,8 +600,8 @@
mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation());
mChildrenContainer.onNotificationUpdated();
}
- if (mIconAnimationRunning) {
- setIconAnimationRunning(true);
+ if (mAnimationRunning) {
+ setAnimationRunning(true);
}
if (mLastChronometerRunning) {
setChronometerRunning(true);
@@ -1038,7 +1077,7 @@
notifyHeightChanged(false /* needsAnimation */);
}
if (pinned) {
- setIconAnimationRunning(true);
+ setAnimationRunning(true);
mExpandedWhenPinned = false;
} else if (mExpandedWhenPinned) {
setUserExpanded(true);
@@ -1158,8 +1197,26 @@
return getShowingLayout().getVisibleWrapper();
}
+ private boolean isNotificationRowLongClickable() {
+ if (mLongPressListener == null) {
+ return false;
+ }
+
+ if (!areGutsExposed()) { // guts is not opened
+ return true;
+ }
+
+ // if it is leave behind, it shouldn't be long clickable.
+ return !isGutsLeaveBehind();
+ }
+
+ private void updateLongClickable() {
+ setLongClickable(isNotificationRowLongClickable());
+ }
+
public void setLongPressListener(LongPressListener longPressListener) {
mLongPressListener = longPressListener;
+ updateLongClickable();
}
public void setDragController(ExpandableNotificationRowDragController dragController) {
@@ -1627,6 +1684,11 @@
);
}
+ /**
+ * Constructs an ExpandableNotificationRow.
+ * @param context context passed to image resolver
+ * @param attrs attributes used to initialize parent view
+ */
public ExpandableNotificationRow(Context context, AttributeSet attrs) {
super(context, attrs);
mImageResolver = new NotificationInlineImageResolver(context,
@@ -1662,7 +1724,8 @@
NotificationGutsManager gutsManager,
MetricsLogger metricsLogger,
SmartReplyConstants smartReplyConstants,
- SmartReplyController smartReplyController) {
+ SmartReplyController smartReplyController,
+ FeatureFlags featureFlags) {
mEntry = entry;
mAppName = appName;
if (mMenuRow == null) {
@@ -1697,6 +1760,7 @@
mBubblesManagerOptional = bubblesManagerOptional;
mNotificationGutsManager = gutsManager;
mMetricsLogger = metricsLogger;
+ mFeatureFlags = featureFlags;
}
private void initDimens() {
@@ -1999,11 +2063,13 @@
void onGutsOpened() {
resetTranslation();
updateContentAccessibilityImportanceForGuts(false /* isEnabled */);
+ updateLongClickable();
}
void onGutsClosed() {
updateContentAccessibilityImportanceForGuts(true /* isEnabled */);
mIsSnoozed = false;
+ updateLongClickable();
}
/**
@@ -2902,6 +2968,10 @@
return (mGuts != null && mGuts.isExposed());
}
+ private boolean isGutsLeaveBehind() {
+ return (mGuts != null && mGuts.isLeavebehind());
+ }
+
@Override
public boolean isContentExpandable() {
if (mIsSummaryWithChildren && !shouldShowPublic()) {
@@ -3588,11 +3658,13 @@
@VisibleForTesting
protected void setPrivateLayout(NotificationContentView privateLayout) {
mPrivateLayout = privateLayout;
+ mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
}
@VisibleForTesting
protected void setPublicLayout(NotificationContentView publicLayout) {
mPublicLayout = publicLayout;
+ mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index d113860..bb92dfc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -219,7 +219,8 @@
mNotificationGutsManager,
mMetricsLogger,
mSmartReplyConstants,
- mSmartReplyController
+ mSmartReplyController,
+ mFeatureFlags
);
mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
if (mAllowLongPress) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
index 49dc655..21f4cb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
@@ -16,15 +16,22 @@
package com.android.systemui.statusbar.notification.row;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.StringRes;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.IndentingPrintWriter;
import android.view.View;
+import android.widget.TextView;
import androidx.annotation.NonNull;
+import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -41,6 +48,11 @@
private String mManageNotificationText;
private String mManageNotificationHistoryText;
+ // Footer label
+ private TextView mSeenNotifsFooterTextView;
+ private @StringRes int mSeenNotifsFilteredText;
+ private int mUnlockIconSize;
+
public FooterView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -73,10 +85,41 @@
super.onFinishInflate();
mClearAllButton = (FooterViewButton) findSecondaryView();
mManageButton = findViewById(R.id.manage_text);
+ mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer);
updateResources();
updateText();
}
+ public void setFooterLabelTextAndIcon(@StringRes int text, @DrawableRes int icon) {
+ mSeenNotifsFilteredText = text;
+ if (mSeenNotifsFilteredText != 0) {
+ mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
+ } else {
+ mSeenNotifsFooterTextView.setText(null);
+ }
+ Drawable drawable;
+ if (icon == 0) {
+ drawable = null;
+ } else {
+ drawable = getResources().getDrawable(icon);
+ drawable.setBounds(0, 0, mUnlockIconSize, mUnlockIconSize);
+ }
+ mSeenNotifsFooterTextView.setCompoundDrawablesRelative(drawable, null, null, null);
+ updateFooterVisibilityMode();
+ }
+
+ private void updateFooterVisibilityMode() {
+ if (mSeenNotifsFilteredText != 0) {
+ mManageButton.setVisibility(View.GONE);
+ mClearAllButton.setVisibility(View.GONE);
+ mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
+ } else {
+ mManageButton.setVisibility(View.VISIBLE);
+ mClearAllButton.setVisibility(View.VISIBLE);
+ mSeenNotifsFooterTextView.setVisibility(View.GONE);
+ }
+ }
+
public void setManageButtonClickListener(OnClickListener listener) {
mManageButton.setOnClickListener(listener);
}
@@ -135,12 +178,19 @@
mClearAllButton.setTextColor(textColor);
mManageButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background));
mManageButton.setTextColor(textColor);
+ final @ColorInt int labelTextColor =
+ Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary);
+ mSeenNotifsFooterTextView.setTextColor(labelTextColor);
+ mSeenNotifsFooterTextView.setCompoundDrawableTintList(
+ ColorStateList.valueOf(labelTextColor));
}
private void updateResources() {
mManageNotificationText = getContext().getString(R.string.manage_notifications_text);
mManageNotificationHistoryText = getContext()
.getString(R.string.manage_notifications_history_text);
+ mUnlockIconSize = getResources()
+ .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 7c21ffb..d93c12b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -184,6 +184,8 @@
private boolean mRemoteInputVisible;
private int mUnrestrictedContentHeight;
+ private boolean mContentAnimating;
+
public NotificationContentView(Context context, AttributeSet attrs) {
super(context, attrs);
mHybridGroupManager = new HybridGroupManager(getContext());
@@ -2129,8 +2131,49 @@
return false;
}
+ /**
+ * Starts and stops animations in the underlying views.
+ * Avoids restarting the animations by checking whether they're already running first.
+ * Return value is used for testing.
+ *
+ * @param running whether to start animations running, or stop them.
+ * @return true if the state of animations changed.
+ */
+ public boolean setContentAnimationRunning(boolean running) {
+ boolean stateChangeRequired = (running != mContentAnimating);
+ if (stateChangeRequired) {
+ // Starts or stops the animations in the potential views.
+ if (mContractedWrapper != null) {
+ mContractedWrapper.setAnimationsRunning(running);
+ }
+ if (mExpandedWrapper != null) {
+ mExpandedWrapper.setAnimationsRunning(running);
+ }
+ if (mHeadsUpWrapper != null) {
+ mHeadsUpWrapper.setAnimationsRunning(running);
+ }
+ // Updates the state tracker.
+ mContentAnimating = running;
+ return true;
+ }
+ return false;
+ }
+
private static class RemoteInputViewData {
@Nullable RemoteInputView mView;
@Nullable RemoteInputViewController mController;
}
+
+ @VisibleForTesting
+ protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) {
+ mContractedWrapper = contractedWrapper;
+ }
+ @VisibleForTesting
+ protected void setExpandedWrapper(NotificationViewWrapper expandedWrapper) {
+ mExpandedWrapper = expandedWrapper;
+ }
+ @VisibleForTesting
+ protected void setHeadsUpWrapper(NotificationViewWrapper headsUpWrapper) {
+ mHeadsUpWrapper = headsUpWrapper;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index ea12b82..efcbb3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -44,12 +44,10 @@
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settingslib.notification.ConversationIconFactory;
-import com.android.systemui.Dependency;
-import com.android.systemui.Dumpable;
import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -65,28 +63,29 @@
import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener;
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
-import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.wmshell.BubblesManager;
-import java.io.PrintWriter;
import java.util.Optional;
+import javax.inject.Inject;
+
import dagger.Lazy;
/**
* Handles various NotificationGuts related tasks, such as binding guts to a row, opening and
* closing guts, and keeping track of the currently exposed notification guts.
*/
+@SysUISingleton
public class NotificationGutsManager implements NotifGutsViewManager {
private static final String TAG = "NotificationGutsManager";
// Must match constant in Settings. Used to highlight preferences when linking to Settings.
private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
- private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+ private final MetricsLogger mMetricsLogger;
private final Context mContext;
private final AccessibilityManager mAccessibilityManager;
private final HighPriorityProvider mHighPriorityProvider;
@@ -94,12 +93,9 @@
private final OnUserInteractionCallback mOnUserInteractionCallback;
// Dependencies:
- private final NotificationLockscreenUserManager mLockscreenUserManager =
- Dependency.get(NotificationLockscreenUserManager.class);
- private final StatusBarStateController mStatusBarStateController =
- Dependency.get(StatusBarStateController.class);
- private final DeviceProvisionedController mDeviceProvisionedController =
- Dependency.get(DeviceProvisionedController.class);
+ private final NotificationLockscreenUserManager mLockscreenUserManager;
+ private final StatusBarStateController mStatusBarStateController;
+ private final DeviceProvisionedController mDeviceProvisionedController;
private final AssistantFeedbackController mAssistantFeedbackController;
// which notification is currently being longpress-examined by the user
@@ -124,9 +120,7 @@
private final ShadeController mShadeController;
private NotifGutsViewListener mGutsListener;
- /**
- * Injected constructor. See {@link NotificationsModule}.
- */
+ @Inject
public NotificationGutsManager(Context context,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
@Main Handler mainHandler,
@@ -143,7 +137,11 @@
Optional<BubblesManager> bubblesManagerOptional,
UiEventLogger uiEventLogger,
OnUserInteractionCallback onUserInteractionCallback,
- ShadeController shadeController) {
+ ShadeController shadeController,
+ NotificationLockscreenUserManager notificationLockscreenUserManager,
+ StatusBarStateController statusBarStateController,
+ DeviceProvisionedController deviceProvisionedController,
+ MetricsLogger metricsLogger) {
mContext = context;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
mMainHandler = mainHandler;
@@ -161,6 +159,10 @@
mUiEventLogger = uiEventLogger;
mOnUserInteractionCallback = onUserInteractionCallback;
mShadeController = shadeController;
+ mLockscreenUserManager = notificationLockscreenUserManager;
+ mStatusBarStateController = statusBarStateController;
+ mDeviceProvisionedController = deviceProvisionedController;
+ mMetricsLogger = metricsLogger;
}
public void setUpWithPresenter(NotificationPresenter presenter,
@@ -372,7 +374,8 @@
mDeviceProvisionedController.isDeviceProvisioned(),
row.getIsNonblockable(),
mHighPriorityProvider.isHighPriority(row.getEntry()),
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
}
/**
@@ -583,7 +586,9 @@
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
- view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ if (view.isLongClickable()) {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
if (row.areGutsExposed()) {
closeAndSaveGuts(false /* removeLeavebehind */, false /* force */,
true /* removeControls */, -1 /* x */, -1 /* y */,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index ea0060a..8a50f2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -204,10 +204,11 @@
boolean isDeviceProvisioned,
boolean isNonblockable,
boolean wasShownHighPriority,
- AssistantFeedbackController assistantFeedbackController)
+ AssistantFeedbackController assistantFeedbackController,
+ MetricsLogger metricsLogger)
throws RemoteException {
mINotificationManager = iNotificationManager;
- mMetricsLogger = Dependency.get(MetricsLogger.class);
+ mMetricsLogger = metricsLogger;
mOnUserInteractionCallback = onUserInteractionCallback;
mChannelEditorDialogController = channelEditorDialogController;
mAssistantFeedbackController = assistantFeedbackController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
index 8732696..175ba15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
@@ -18,11 +18,15 @@
import android.app.Notification;
import android.content.Context;
+import android.graphics.drawable.AnimatedImageDrawable;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.view.View;
+import com.android.internal.R;
+import com.android.internal.widget.BigPictureNotificationImageView;
import com.android.systemui.statusbar.notification.ImageTransformState;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -31,6 +35,8 @@
*/
public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper {
+ private BigPictureNotificationImageView mImageView;
+
protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view,
ExpandableNotificationRow row) {
super(ctx, view, row);
@@ -39,9 +45,14 @@
@Override
public void onContentUpdated(ExpandableNotificationRow row) {
super.onContentUpdated(row);
+ resolveViews();
updateImageTag(row.getEntry().getSbn());
}
+ private void resolveViews() {
+ mImageView = mView.findViewById(R.id.big_picture);
+ }
+
private void updateImageTag(StatusBarNotification sbn) {
final Bundle extras = sbn.getNotification().extras;
Icon bigLargeIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG, Icon.class);
@@ -54,4 +65,25 @@
mRightIcon.setTag(ImageTransformState.ICON_TAG, getLargeIcon(sbn.getNotification()));
}
}
+
+ /**
+ * Starts or stops the animations in any drawables contained in this BigPicture Notification.
+ *
+ * @param running Whether the animations should be set to run.
+ */
+ @Override
+ public void setAnimationsRunning(boolean running) {
+ if (mImageView == null) {
+ return;
+ }
+ Drawable d = mImageView.getDrawable();
+ if (d instanceof AnimatedImageDrawable) {
+ AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d;
+ if (running) {
+ animatedImageDrawable.start();
+ } else {
+ animatedImageDrawable.stop();
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
index e136055..10753f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
@@ -17,16 +17,20 @@
package com.android.systemui.statusbar.notification.row.wrapper
import android.content.Context
+import android.graphics.drawable.AnimatedImageDrawable
import android.view.View
import android.view.ViewGroup
import com.android.internal.widget.CachingIconView
import com.android.internal.widget.ConversationLayout
+import com.android.internal.widget.MessagingGroup
+import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLinearLayout
import com.android.systemui.R
import com.android.systemui.statusbar.notification.NotificationFadeAware
import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform
+import com.android.systemui.util.children
/**
* Wraps a notification containing a conversation template
@@ -49,6 +53,7 @@
private lateinit var expandBtn: View
private lateinit var expandBtnContainer: View
private lateinit var imageMessageContainer: ViewGroup
+ private lateinit var messageContainers: ArrayList<MessagingGroup>
private lateinit var messagingLinearLayout: MessagingLinearLayout
private lateinit var conversationTitleView: View
private lateinit var importanceRing: View
@@ -60,6 +65,7 @@
private fun resolveViews() {
messagingLinearLayout = conversationLayout.messagingLinearLayout
imageMessageContainer = conversationLayout.imageMessageContainer
+ messageContainers = conversationLayout.messagingGroups
with(conversationLayout) {
conversationIconContainer =
requireViewById(com.android.internal.R.id.conversation_icon_container)
@@ -146,4 +152,26 @@
NotificationFadeAware.setLayerTypeForFaded(expandBtn, faded)
NotificationFadeAware.setLayerTypeForFaded(conversationIconContainer, faded)
}
+
+ // Starts or stops the animations in any drawables contained in this Conversation Notification.
+ override fun setAnimationsRunning(running: Boolean) {
+ // We apply to both the child message containers in a conversation group,
+ // and the top level image message container.
+ val containers = messageContainers.asSequence().map { it.messageContainer } +
+ sequenceOf(imageMessageContainer)
+ val drawables =
+ containers
+ .flatMap { it.children }
+ .mapNotNull { child ->
+ (child as? MessagingImageMessage)?.let { imageMessage ->
+ imageMessage.drawable as? AnimatedImageDrawable
+ }
+ }
+ drawables.toSet().forEach {
+ when {
+ running -> it.start()
+ !running -> it.stop()
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java
index c587ce0..4592fde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java
@@ -17,9 +17,13 @@
package com.android.systemui.statusbar.notification.row.wrapper;
import android.content.Context;
+import android.graphics.drawable.AnimatedImageDrawable;
+import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
+import com.android.internal.widget.MessagingGroup;
+import com.android.internal.widget.MessagingImageMessage;
import com.android.internal.widget.MessagingLayout;
import com.android.internal.widget.MessagingLinearLayout;
import com.android.systemui.R;
@@ -127,4 +131,40 @@
}
return super.getMinLayoutHeight();
}
+
+ /**
+ * Starts or stops the animations in any drawables contained in this Messaging Notification.
+ *
+ * @param running Whether the animations should be set to run.
+ */
+ @Override
+ public void setAnimationsRunning(boolean running) {
+ if (mMessagingLayout == null) {
+ return;
+ }
+
+ for (MessagingGroup group : mMessagingLayout.getMessagingGroups()) {
+ for (int i = 0; i < group.getMessageContainer().getChildCount(); i++) {
+ View view = group.getMessageContainer().getChildAt(i);
+ // We only need to set animations in MessagingImageMessages.
+ if (!(view instanceof MessagingImageMessage)) {
+ continue;
+ }
+ MessagingImageMessage imageMessage =
+ (com.android.internal.widget.MessagingImageMessage) view;
+
+ // If the drawable isn't an AnimatedImageDrawable, we can't set it to animate.
+ Drawable d = imageMessage.getDrawable();
+ if (!(d instanceof AnimatedImageDrawable)) {
+ continue;
+ }
+ AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d;
+ if (running) {
+ animatedImageDrawable.start();
+ } else {
+ animatedImageDrawable.stop();
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index 1c22f09..ff5b9cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -403,4 +403,12 @@
NotificationFadeAware.setLayerTypeForFaded(getIcon(), faded);
NotificationFadeAware.setLayerTypeForFaded(getExpandButton(), faded);
}
+
+ /**
+ * Starts or stops the animations in any drawables contained in this Notification.
+ *
+ * @param running Whether the animations should be set to run.
+ */
+ public void setAnimationsRunning(boolean running) {
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 8d48d73..9b93d7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -1431,6 +1431,22 @@
@Override
public void applyRoundnessAndInvalidate() {
boolean last = true;
+ if (mUseRoundnessSourceTypes) {
+ if (mNotificationHeaderWrapper != null) {
+ mNotificationHeaderWrapper.requestTopRoundness(
+ /* value = */ getTopRoundness(),
+ /* sourceType = */ FROM_PARENT,
+ /* animate = */ false
+ );
+ }
+ if (mNotificationHeaderWrapperLowPriority != null) {
+ mNotificationHeaderWrapperLowPriority.requestTopRoundness(
+ /* value = */ getTopRoundness(),
+ /* sourceType = */ FROM_PARENT,
+ /* animate = */ false
+ );
+ }
+ }
for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
if (child.getVisibility() == View.GONE) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index ca1e397..aab36da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -538,6 +538,7 @@
private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
private final ScreenOffAnimationController mScreenOffAnimationController;
private boolean mShouldUseSplitNotificationShade;
+ private boolean mHasFilteredOutSeenNotifications;
private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener =
new ExpandableView.OnHeightChangedListener() {
@@ -684,6 +685,10 @@
updateFooter();
}
+ void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
+ mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications;
+ }
+
@VisibleForTesting
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void updateFooter() {
@@ -1811,9 +1816,7 @@
@Override
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
- mBottomInset = insets.getSystemWindowInsetBottom()
- + insets.getInsets(WindowInsets.Type.ime()).bottom;
-
+ mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom;
mWaterfallTopInset = 0;
final DisplayCutout cutout = insets.getDisplayCutout();
if (cutout != null) {
@@ -2262,7 +2265,11 @@
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private int getImeInset() {
- return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight()));
+ // The NotificationStackScrollLayout does not extend all the way to the bottom of the
+ // display. Therefore, subtract that space from the mBottomInset, in order to only include
+ // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout.
+ return Math.max(0, mBottomInset
+ - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1]));
}
/**
@@ -2970,12 +2977,19 @@
childInGroup = (ExpandableNotificationRow) requestedView;
requestedView = requestedRow = childInGroup.getNotificationParent();
}
- int position = 0;
+ final float scrimTopPadding = mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings;
+ int position = (int) scrimTopPadding;
+ int visibleIndex = -1;
+ ExpandableView lastVisibleChild = null;
for (int i = 0; i < getChildCount(); i++) {
ExpandableView child = getChildAtIndex(i);
boolean notGone = child.getVisibility() != View.GONE;
+ if (notGone) visibleIndex++;
if (notGone && !child.hasNoContentHeight()) {
- if (position != 0) {
+ if (position != scrimTopPadding) {
+ if (lastVisibleChild != null) {
+ position += calculateGapHeight(lastVisibleChild, child, visibleIndex);
+ }
position += mPaddingBetweenElements;
}
}
@@ -2987,6 +3001,7 @@
}
if (notGone) {
position += getIntrinsicHeight(child);
+ lastVisibleChild = child;
}
}
return 0;
@@ -3122,7 +3137,7 @@
private void updateAnimationState(boolean running, View child) {
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
- row.setIconAnimationRunning(running);
+ row.setAnimationRunning(running);
}
}
@@ -4602,13 +4617,12 @@
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
- void updateEmptyShadeView(
- boolean visible, boolean areNotificationsHiddenInShade, boolean areSeenNotifsFiltered) {
+ void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
if (areNotificationsHiddenInShade) {
updateEmptyShadeView(R.string.dnd_suppressing_shade_text, 0, 0);
- } else if (areSeenNotifsFiltered) {
+ } else if (mHasFilteredOutSeenNotifications) {
updateEmptyShadeView(
R.string.no_unseen_notif_text,
R.string.unlock_to_see_notif_text,
@@ -4647,13 +4661,20 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) {
- if (mFooterView == null) {
+ if (mFooterView == null || mNotificationStackSizeCalculator == null) {
return;
}
boolean animate = mIsExpanded && mAnimationsEnabled;
mFooterView.setVisible(visible, animate);
mFooterView.setSecondaryVisible(showDismissView, animate);
mFooterView.showHistory(showHistory);
+ if (mHasFilteredOutSeenNotifications) {
+ mFooterView.setFooterLabelTextAndIcon(
+ R.string.unlock_to_see_notif_text,
+ R.drawable.ic_friction_lock_closed);
+ } else {
+ mFooterView.setFooterLabelTextAndIcon(0, 0);
+ }
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5891948..42d122d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -120,6 +120,7 @@
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.Compile;
+import com.android.systemui.util.settings.SecureSettings;
import java.util.ArrayList;
import java.util.List;
@@ -189,6 +190,7 @@
private final FeatureFlags mFeatureFlags;
private final boolean mUseRoundnessSourceTypes;
private final NotificationTargetsHelper mNotificationTargetsHelper;
+ private final SecureSettings mSecureSettings;
private View mLongPressedView;
@@ -667,7 +669,8 @@
NotificationStackScrollLogger logger,
NotificationStackSizeCalculator notificationStackSizeCalculator,
FeatureFlags featureFlags,
- NotificationTargetsHelper notificationTargetsHelper) {
+ NotificationTargetsHelper notificationTargetsHelper,
+ SecureSettings secureSettings) {
mStackStateLogger = stackLogger;
mLogger = logger;
mAllowLongPress = allowLongPress;
@@ -709,6 +712,7 @@
mFeatureFlags = featureFlags;
mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
mNotificationTargetsHelper = notificationTargetsHelper;
+ mSecureSettings = secureSettings;
updateResources();
}
@@ -1015,8 +1019,7 @@
Log.wtf(TAG, "isHistoryEnabled failed to initialize its value");
return false;
}
- mHistoryEnabled = historyEnabled = Settings.Secure.getIntForUser(
- mView.getContext().getContentResolver(),
+ mHistoryEnabled = historyEnabled = mSecureSettings.getIntForUser(
Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
0,
UserHandle.USER_CURRENT) == 1;
@@ -1242,11 +1245,7 @@
// For more details, see: b/228790482
&& !isInTransitionToKeyguard();
- mView.updateEmptyShadeView(
- shouldShow,
- mZenModeController.areNotificationsHiddenInShade(),
- mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()
- && mSeenNotificationsProvider.getHasFilteredOutSeenNotifications());
+ mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade());
Trace.endSection();
}
@@ -1942,6 +1941,9 @@
@Override
public void setNotifStats(@NonNull NotifStats notifStats) {
mNotifStats = notifStats;
+ mView.setHasFilteredOutSeenNotifications(
+ mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()
+ && mSeenNotificationsProvider.getHasFilteredOutSeenNotifications());
updateFooter();
updateShowEmptyShadeView();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
index 3ccef9d..eb81c46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
@@ -16,25 +16,35 @@
package com.android.systemui.statusbar.phone;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
+
import android.content.Context;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.view.IWindowManager;
import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.NonNull;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.AutoHideUiElement;
+import java.io.PrintWriter;
+
import javax.inject.Inject;
/** A controller to control all auto-hide things. Also see {@link AutoHideUiElement}. */
@SysUISingleton
public class AutoHideController {
private static final String TAG = "AutoHideController";
- private static final long AUTO_HIDE_TIMEOUT_MS = 2250;
+ private static final int AUTO_HIDE_TIMEOUT_MS = 2250;
+ private static final int USER_AUTO_HIDE_TIMEOUT_MS = 350;
+ private final AccessibilityManager mAccessibilityManager;
private final IWindowManager mWindowManagerService;
private final Handler mHandler;
@@ -52,11 +62,12 @@
};
@Inject
- public AutoHideController(Context context, @Main Handler handler,
+ public AutoHideController(Context context,
+ @Main Handler handler,
IWindowManager iWindowManager) {
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mHandler = handler;
mWindowManagerService = iWindowManager;
-
mDisplayId = context.getDisplayId();
}
@@ -138,7 +149,12 @@
private void scheduleAutoHide() {
cancelAutoHide();
- mHandler.postDelayed(mAutoHide, AUTO_HIDE_TIMEOUT_MS);
+ mHandler.postDelayed(mAutoHide, getAutoHideTimeout());
+ }
+
+ private int getAutoHideTimeout() {
+ return mAccessibilityManager.getRecommendedTimeoutMillis(AUTO_HIDE_TIMEOUT_MS,
+ FLAG_CONTENT_CONTROLS);
}
public void checkUserAutoHide(MotionEvent event) {
@@ -160,7 +176,13 @@
private void userAutoHide() {
cancelAutoHide();
- mHandler.postDelayed(mAutoHide, 350); // longer than app gesture -> flag clear
+ // longer than app gesture -> flag clear
+ mHandler.postDelayed(mAutoHide, getUserAutoHideTimeout());
+ }
+
+ private int getUserAutoHideTimeout() {
+ return mAccessibilityManager.getRecommendedTimeoutMillis(USER_AUTO_HIDE_TIMEOUT_MS,
+ FLAG_CONTENT_CONTROLS);
}
private boolean isAnyTransientBarShown() {
@@ -175,6 +197,15 @@
return false;
}
+ public void dump(@NonNull PrintWriter pw) {
+ pw.println("AutoHideController:");
+ pw.println("\tmAutoHideSuspended=" + mAutoHideSuspended);
+ pw.println("\tisAnyTransientBarShown=" + isAnyTransientBarShown());
+ pw.println("\thasPendingAutoHide=" + mHandler.hasCallbacks(mAutoHide));
+ pw.println("\tgetAutoHideTimeout=" + getAutoHideTimeout());
+ pw.println("\tgetUserAutoHideTimeout=" + getUserAutoHideTimeout());
+ }
+
/**
* Injectable factory for creating a {@link AutoHideController}.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
index 9070ead..8b1a02b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -154,9 +154,7 @@
if (!mAutoTracker.isAdded(SAVER)) {
mDataSaverController.addCallback(mDataSaverListener);
}
- if (!mAutoTracker.isAdded(WORK)) {
- mManagedProfileController.addCallback(mProfileCallback);
- }
+ mManagedProfileController.addCallback(mProfileCallback);
if (!mAutoTracker.isAdded(NIGHT)
&& ColorDisplayManager.isNightDisplayAvailable(mContext)) {
mNightDisplayListener.setCallback(mNightDisplayCallback);
@@ -275,18 +273,19 @@
return mCurrentUser.getIdentifier();
}
- public void unmarkTileAsAutoAdded(String tabSpec) {
- mAutoTracker.setTileRemoved(tabSpec);
- }
-
private final ManagedProfileController.Callback mProfileCallback =
new ManagedProfileController.Callback() {
@Override
public void onManagedProfileChanged() {
- if (mAutoTracker.isAdded(WORK)) return;
if (mManagedProfileController.hasActiveProfile()) {
- mHost.addTile(WORK);
+ if (mAutoTracker.isAdded(WORK)) return;
+ final int position = mAutoTracker.getRestoredTilePosition(WORK);
+ mHost.addTile(WORK, position);
mAutoTracker.setTileAdded(WORK);
+ } else {
+ if (!mAutoTracker.isAdded(WORK)) return;
+ mHost.removeTile(WORK);
+ mAutoTracker.setTileRemoved(WORK);
}
}
@@ -429,7 +428,7 @@
initSafetyTile();
} else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) {
mHost.removeTile(mSafetySpec);
- mHost.unmarkTileAsAutoAdded(mSafetySpec);
+ mAutoTracker.setTileRemoved(mSafetySpec);
}
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 895a293..db2c0a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -18,6 +18,8 @@
import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME;
+
import android.annotation.IntDef;
import android.content.res.Resources;
import android.hardware.biometrics.BiometricFaceConstants;
@@ -27,7 +29,6 @@
import android.metrics.LogMaker;
import android.os.Handler;
import android.os.PowerManager;
-import android.os.SystemClock;
import android.os.Trace;
import androidx.annotation.Nullable;
@@ -62,6 +63,7 @@
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.time.SystemClock;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -78,6 +80,7 @@
*/
@SysUISingleton
public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable {
+ private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L;
private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000;
private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
@@ -169,9 +172,11 @@
private final MetricsLogger mMetricsLogger;
private final AuthController mAuthController;
private final StatusBarStateController mStatusBarStateController;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
private final LatencyTracker mLatencyTracker;
private final VibratorHelper mVibratorHelper;
private final BiometricUnlockLogger mLogger;
+ private final SystemClock mSystemClock;
private long mLastFpFailureUptimeMillis;
private int mNumConsecutiveFpFailures;
@@ -279,14 +284,17 @@
SessionTracker sessionTracker,
LatencyTracker latencyTracker,
ScreenOffAnimationController screenOffAnimationController,
- VibratorHelper vibrator) {
+ VibratorHelper vibrator,
+ SystemClock systemClock
+ ) {
mPowerManager = powerManager;
mShadeController = shadeController;
mUpdateMonitor = keyguardUpdateMonitor;
mUpdateMonitor.registerCallback(this);
mMediaManager = notificationMediaManager;
mLatencyTracker = latencyTracker;
- wakefulnessLifecycle.addObserver(mWakefulnessObserver);
+ mWakefulnessLifecycle = wakefulnessLifecycle;
+ mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
screenLifecycle.addObserver(mScreenObserver);
mNotificationShadeWindowController = notificationShadeWindowController;
@@ -306,6 +314,7 @@
mScreenOffAnimationController = screenOffAnimationController;
mVibratorHelper = vibrator;
mLogger = biometricUnlockLogger;
+ mSystemClock = systemClock;
dumpManager.registerDumpable(getClass().getName(), this);
}
@@ -429,8 +438,11 @@
Runnable wakeUp = ()-> {
if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
mLogger.i("bio wakelock: Authenticated, waking up...");
- mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
- "android.policy:BIOMETRIC");
+ mPowerManager.wakeUp(
+ mSystemClock.uptimeMillis(),
+ PowerManager.WAKE_REASON_BIOMETRIC,
+ "android.policy:BIOMETRIC"
+ );
}
Trace.beginSection("release wake-and-unlock");
releaseBiometricWakeLock();
@@ -670,7 +682,7 @@
startWakeAndUnlock(MODE_ONLY_WAKE);
} else if (biometricSourceType == BiometricSourceType.FINGERPRINT
&& mUpdateMonitor.isUdfpsSupported()) {
- long currUptimeMillis = SystemClock.uptimeMillis();
+ long currUptimeMillis = mSystemClock.uptimeMillis();
if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) {
mNumConsecutiveFpFailures += 1;
} else {
@@ -718,12 +730,26 @@
cleanup();
}
- //these haptics are for device-entry only
+ // these haptics are for device-entry only
private void vibrateSuccess(BiometricSourceType type) {
+ if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())
+ && lastWakeupFromPowerButtonWithinHapticThreshold()) {
+ mLogger.d("Skip auth success haptic. Power button was recently pressed.");
+ return;
+ }
mVibratorHelper.vibrateAuthSuccess(
getClass().getSimpleName() + ", type =" + type + "device-entry::success");
}
+ private boolean lastWakeupFromPowerButtonWithinHapticThreshold() {
+ final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason()
+ == PowerManager.WAKE_REASON_POWER_BUTTON;
+ return lastWakeupFromPowerButton
+ && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME
+ && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime()
+ < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS;
+ }
+
private void vibrateError(BiometricSourceType type) {
mVibratorHelper.vibrateAuthError(
getClass().getSimpleName() + ", type =" + type + "device-entry::error");
@@ -816,7 +842,7 @@
if (mUpdateMonitor.isUdfpsSupported()) {
pw.print(" mNumConsecutiveFpFailures="); pw.println(mNumConsecutiveFpFailures);
pw.print(" time since last failure=");
- pw.println(SystemClock.uptimeMillis() - mLastFpFailureUptimeMillis);
+ pw.println(mSystemClock.uptimeMillis() - mLastFpFailureUptimeMillis);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 2dad8e0..ccde3c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -31,7 +31,6 @@
import android.os.Bundle;
import android.os.PowerManager;
import android.os.SystemClock;
-import android.os.UserHandle;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -55,6 +54,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.CameraLauncher;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.ShadeController;
@@ -99,6 +99,7 @@
private final Optional<Vibrator> mVibratorOptional;
private final DisableFlagsLogger mDisableFlagsLogger;
private final int mDisplayId;
+ private final UserTracker mUserTracker;
private final boolean mVibrateOnOpening;
private final VibrationEffect mCameraLaunchGestureVibrationEffect;
private final SystemBarAttributesListener mSystemBarAttributesListener;
@@ -133,7 +134,8 @@
DisableFlagsLogger disableFlagsLogger,
@DisplayId int displayId,
SystemBarAttributesListener systemBarAttributesListener,
- Lazy<CameraLauncher> cameraLauncherLazy) {
+ Lazy<CameraLauncher> cameraLauncherLazy,
+ UserTracker userTracker) {
mCentralSurfaces = centralSurfaces;
mContext = context;
mShadeController = shadeController;
@@ -157,6 +159,7 @@
mDisableFlagsLogger = disableFlagsLogger;
mDisplayId = displayId;
mCameraLauncherLazy = cameraLauncherLazy;
+ mUserTracker = userTracker;
mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -215,7 +218,7 @@
return;
}
- mNotificationPanelViewController.expandWithoutQs();
+ mNotificationPanelViewController.expandShadeToNotifications();
}
@Override
@@ -375,7 +378,7 @@
mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent,
false /* onlyProvisioned */, true /* dismissShade */,
true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
- null /* animationController */, UserHandle.CURRENT);
+ null /* animationController */, mUserTracker.getUserHandle());
} else {
if (!mCentralSurfaces.isDeviceInteractive()) {
// Avoid flickering of the scrim when we instant launch the camera and the bouncer
@@ -432,7 +435,7 @@
mCentralSurfaces.startActivityDismissingKeyguard(emergencyIntent,
false /* onlyProvisioned */, true /* dismissShade */,
true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
- null /* animationController */, UserHandle.CURRENT);
+ null /* animationController */, mUserTracker.getUserHandle());
return;
}
@@ -447,7 +450,7 @@
if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
mStatusBarKeyguardViewManager.reset(true /* hide */);
}
- mContext.startActivityAsUser(emergencyIntent, UserHandle.CURRENT);
+ mContext.startActivityAsUser(emergencyIntent, mUserTracker.getUserHandle());
return;
}
// We need to defer the emergency action launch until the screen comes on, since otherwise
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 688ce7b..057e4ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -64,7 +64,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
@@ -162,6 +161,7 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder;
import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
import com.android.systemui.navigationbar.NavigationBarController;
@@ -179,6 +179,7 @@
import com.android.systemui.qs.QSPanelController;
import com.android.systemui.recents.ScreenPinningRequest;
import com.android.systemui.scrim.ScrimView;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.settings.brightness.BrightnessSliderController;
import com.android.systemui.shade.CameraLauncher;
import com.android.systemui.shade.NotificationPanelViewController;
@@ -291,26 +292,9 @@
private static final UiEventLogger sUiEventLogger = new UiEventLoggerImpl();
- /**
- * If true, the system is in the half-boot-to-decryption-screen state.
- * Prudently disable QS and notifications.
- */
- public static final boolean ONLY_CORE_APPS;
-
- static {
- boolean onlyCoreApps;
- try {
- IPackageManager packageManager =
- IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
- onlyCoreApps = packageManager != null && packageManager.isOnlyCoreApps();
- } catch (RemoteException e) {
- onlyCoreApps = false;
- }
- ONLY_CORE_APPS = onlyCoreApps;
- }
-
private final Context mContext;
private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+ private final DeviceStateManager mDeviceStateManager;
private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
private float mTransitionToFullShadeProgress = 0f;
private NotificationListContainer mNotifListContainer;
@@ -485,6 +469,7 @@
private final ShadeController mShadeController;
private final InitController mInitController;
private final Lazy<CameraLauncher> mCameraLauncherLazy;
+ private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final PluginDependencyProvider mPluginDependencyProvider;
private final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -521,6 +506,7 @@
private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
private final MessageRouter mMessageRouter;
private final WallpaperManager mWallpaperManager;
+ private final UserTracker mUserTracker;
private CentralSurfacesComponent mCentralSurfacesComponent;
@@ -763,7 +749,10 @@
WiredChargingRippleController wiredChargingRippleController,
IDreamManager dreamManager,
Lazy<CameraLauncher> cameraLauncherLazy,
- Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy) {
+ Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy,
+ AlternateBouncerInteractor alternateBouncerInteractor,
+ UserTracker userTracker
+ ) {
mContext = context;
mNotificationsController = notificationsController;
mFragmentService = fragmentService;
@@ -841,6 +830,8 @@
mWallpaperManager = wallpaperManager;
mJankMonitor = jankMonitor;
mCameraLauncherLazy = cameraLauncherLazy;
+ mAlternateBouncerInteractor = alternateBouncerInteractor;
+ mUserTracker = userTracker;
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
mStartingSurfaceOptional = startingSurfaceOptional;
@@ -874,11 +865,15 @@
mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT,
id -> onLaunchTransitionTimeout());
- deviceStateManager.registerCallback(mMainExecutor,
- new FoldStateListener(mContext, this::onFoldedStateChanged));
+ mDeviceStateManager = deviceStateManager;
wiredChargingRippleController.registerCallbacks();
mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
+
+ // Based on teamfood flag, turn predictive back dispatch on at runtime.
+ if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) {
+ mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
+ }
}
@Override
@@ -993,11 +988,6 @@
// Lastly, call to the icon policy to install/update all the icons.
mIconPolicy.init();
- // Based on teamfood flag, turn predictive back dispatch on at runtime.
- if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) {
- mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
- }
-
mKeyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
public void onUnlockedChanged() {
@@ -1064,6 +1054,8 @@
}
});
+ registerCallbacks();
+
mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
mPluginManager.addPluginListener(
@@ -1119,6 +1111,14 @@
}
@VisibleForTesting
+ /** Registers listeners/callbacks with external dependencies. */
+ void registerCallbacks() {
+ //TODO(b/264502026) move the rest of the listeners here.
+ mDeviceStateManager.registerCallback(mMainExecutor,
+ new FoldStateListener(mContext, this::onFoldedStateChanged));
+ }
+
+ @VisibleForTesting
void initShadeVisibilityListener() {
mShadeController.setVisibilityListener(new ShadeController.ShadeVisibilityListener() {
@Override
@@ -1306,8 +1306,13 @@
// Set up the quick settings tile panel
final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame);
if (container != null) {
- FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
- ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame,
+ FragmentHostManager fragmentHostManager =
+ mFragmentService.getFragmentHostManager(container);
+ ExtensionFragmentListener.attachExtensonToFragment(
+ mFragmentService,
+ container,
+ QS.TAG,
+ R.id.qs_frame,
mExtensionController
.newExtension(QS.class)
.withPlugin(QS.class)
@@ -1399,6 +1404,7 @@
// Things that mean we're not swiping to dismiss the keyguard, and should ignore this
// expansion:
// - Keyguard isn't even visible.
+ // - We're swiping on the bouncer, not the lockscreen.
// - Keyguard is occluded. Expansion changes here are the shade being expanded over the
// occluding activity.
// - Keyguard is visible, but can't be dismissed (swiping up will show PIN/password prompt).
@@ -1407,11 +1413,14 @@
// to dismiss the lock screen until entering the SIM PIN.
// - QS is expanded and we're swiping - swiping up now will hide QS, not dismiss the
// keyguard.
+ // - Shade is in QQS over keyguard - swiping up should take us back to keyguard
if (!isKeyguardShowing()
+ || mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
|| isOccluded()
|| !mKeyguardStateController.canDismissLockScreen()
|| mKeyguardViewMediator.isAnySimPinSecure()
- || (mNotificationPanelViewController.isQsExpanded() && trackingTouch)) {
+ || (mNotificationPanelViewController.isQsExpanded() && trackingTouch)
+ || mNotificationPanelViewController.getBarState() == StatusBarState.SHADE_LOCKED) {
return;
}
@@ -1474,7 +1483,9 @@
}
protected QS createDefaultQSFragment() {
- return FragmentHostManager.get(mNotificationShadeWindowView).create(QSFragment.class);
+ return mFragmentService
+ .getFragmentHostManager(mNotificationShadeWindowView)
+ .create(QSFragment.class);
}
private void setUpPresenter() {
@@ -1687,8 +1698,7 @@
|| !mUserSwitcherController.isSimpleUserSwitcher())
&& !isShadeDisabled()
&& ((mDisabled2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) == 0)
- && !mDozing
- && !ONLY_CORE_APPS;
+ && !mDozing;
mNotificationPanelViewController.setQsExpansionEnabledPolicy(expandEnabled);
Log.d(TAG, "updateQsExpansionEnabled - QS Expand enabled: " + expandEnabled);
}
@@ -3257,8 +3267,7 @@
private void showBouncerOrLockScreenIfKeyguard() {
// If the keyguard is animating away, we aren't really the keyguard anymore and should not
// show the bouncer/lockscreen.
- if (!mKeyguardViewMediator.isHiding()
- && !mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
+ if (!mKeyguardViewMediator.isHiding() && !mKeyguardUpdateMonitor.isKeyguardGoingAway()) {
if (mState == StatusBarState.SHADE_LOCKED) {
// shade is showing while locked on the keyguard, so go back to showing the
// lock screen where users can use the UDFPS affordance to enter the device
@@ -3738,7 +3747,7 @@
boolean launchingAffordanceWithPreview = mLaunchingAffordance;
mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview);
- if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.isVisibleState()) {
if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED
|| mTransitionToFullShadeProgress > 0f) {
mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
@@ -3771,6 +3780,9 @@
});
} else if (mDozing && !unlocking) {
mScrimController.transitionTo(ScrimState.AOD);
+ // This will cancel the keyguardFadingAway animation if it is running. We need to do
+ // this as otherwise it can remain pending and leave keyguard in a weird state.
+ mUnlockScrimCallback.onCancelled();
} else if (mKeyguardStateController.isShowing() && !isOccluded() && !unlocking) {
mScrimController.transitionTo(ScrimState.KEYGUARD);
} else if (mKeyguardStateController.isShowing() && mKeyguardUpdateMonitor.isDreaming()
@@ -4159,7 +4171,7 @@
Log.wtf(TAG, "WallpaperManager not supported");
return;
}
- WallpaperInfo info = mWallpaperManager.getWallpaperInfo(UserHandle.USER_CURRENT);
+ WallpaperInfo info = mWallpaperManager.getWallpaperInfo(mUserTracker.getUserId());
mWallpaperController.onWallpaperInfoUpdated(info);
final boolean deviceSupportsAodWallpaper = mContext.getResources().getBoolean(
@@ -4263,8 +4275,7 @@
@Override
public void onDozeAmountChanged(float linear, float eased) {
- if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)
- && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)
+ if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)
&& !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) {
mLightRevealScrim.setRevealAmount(1f - linear);
}
@@ -4384,6 +4395,6 @@
return new UserHandle(UserHandle.myUserId());
}
}
- return UserHandle.CURRENT;
+ return mUserTracker.getUserHandle();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index de7b152..c248a50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -44,10 +44,10 @@
import com.android.systemui.doze.AlwaysOnDisplayPolicy;
import com.android.systemui.doze.DozeScreenState;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.tuner.TunerService;
@@ -82,10 +82,10 @@
private final AlwaysOnDisplayPolicy mAlwaysOnPolicy;
private final Resources mResources;
private final BatteryController mBatteryController;
- private final FeatureFlags mFeatureFlags;
private final ScreenOffAnimationController mScreenOffAnimationController;
private final FoldAodAnimationController mFoldAodAnimationController;
private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+ private final UserTracker mUserTracker;
private final Set<Callback> mCallbacks = new HashSet<>();
@@ -125,13 +125,13 @@
BatteryController batteryController,
TunerService tunerService,
DumpManager dumpManager,
- FeatureFlags featureFlags,
ScreenOffAnimationController screenOffAnimationController,
Optional<SysUIUnfoldComponent> sysUiUnfoldComponent,
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
ConfigurationController configurationController,
- StatusBarStateController statusBarStateController) {
+ StatusBarStateController statusBarStateController,
+ UserTracker userTracker) {
mResources = resources;
mAmbientDisplayConfiguration = ambientDisplayConfiguration;
mAlwaysOnPolicy = alwaysOnDisplayPolicy;
@@ -141,9 +141,9 @@
mControlScreenOffAnimation = !getDisplayNeedsBlanking();
mPowerManager = powerManager;
mPowerManager.setDozeAfterScreenOff(!mControlScreenOffAnimation);
- mFeatureFlags = featureFlags;
mScreenOffAnimationController = screenOffAnimationController;
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
+ mUserTracker = userTracker;
keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
tunerService.addTunable(
@@ -162,11 +162,18 @@
SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler);
quickPickupSettingsObserver.observe();
+
+ batteryController.addCallback(new BatteryStateChangeCallback() {
+ @Override
+ public void onPowerSaveChanged(boolean isPowerSave) {
+ dispatchAlwaysOnEvent();
+ }
+ });
}
private void updateQuickPickupEnabled() {
mIsQuickPickupEnabled =
- mAmbientDisplayConfiguration.quickPickupSensorEnabled(UserHandle.USER_CURRENT);
+ mAmbientDisplayConfiguration.quickPickupSensorEnabled(mUserTracker.getUserId());
}
public boolean getDisplayStateSupported() {
@@ -300,13 +307,10 @@
/**
* Whether we're capable of controlling the screen off animation if we want to. This isn't
- * possible if AOD isn't even enabled or if the flag is disabled, or if the display needs
- * blanking.
+ * possible if AOD isn't even enabled or if the display needs blanking.
*/
public boolean canControlUnlockedScreenOff() {
- return getAlwaysOn()
- && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)
- && !getDisplayNeedsBlanking();
+ return getAlwaysOn() && !getDisplayNeedsBlanking();
}
/**
@@ -418,15 +422,13 @@
@Override
public void onTuningChanged(String key, String newValue) {
- mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
+ mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(mUserTracker.getUserId());
if (key.equals(Settings.Secure.DOZE_ALWAYS_ON)) {
updateControlScreenOff();
}
- for (Callback callback : mCallbacks) {
- callback.onAlwaysOnChange();
- }
+ dispatchAlwaysOnEvent();
mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn());
}
@@ -463,6 +465,12 @@
pw.print("isQuickPickupEnabled(): "); pw.println(isQuickPickupEnabled());
}
+ private void dispatchAlwaysOnEvent() {
+ for (Callback callback : mCallbacks) {
+ callback.onAlwaysOnChange();
+ }
+ }
+
private boolean getPostureSpecificBool(
int[] postureMapping,
boolean defaultSensorBool,
@@ -477,7 +485,8 @@
return bool;
}
- interface Callback {
+ /** Callbacks for doze parameter related information */
+ public interface Callback {
/**
* Invoked when the value of getAlwaysOn may have changed.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
deleted file mode 100644
index 000fe14..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ /dev/null
@@ -1,762 +0,0 @@
-/*
- * Copyright (C) 2014 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.statusbar.phone;
-
-import static com.android.keyguard.KeyguardSecurityModel.SecurityMode;
-import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.hardware.biometrics.BiometricSourceType;
-import android.os.Handler;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-
-import com.android.internal.policy.SystemBarUtils;
-import com.android.keyguard.KeyguardHostViewController;
-import com.android.keyguard.KeyguardSecurityModel;
-import com.android.keyguard.KeyguardSecurityView;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.ViewMediatorCallback;
-import com.android.keyguard.dagger.KeyguardBouncerComponent;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
-import com.android.systemui.shared.system.SysUiStatsLog;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.ListenerSet;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * A class which manages the primary (pin/pattern/password) bouncer on the lockscreen.
- * @deprecated Use KeyguardBouncerRepository
- */
-@Deprecated
-public class KeyguardBouncer {
-
- private static final String TAG = "PrimaryKeyguardBouncer";
- static final long BOUNCER_FACE_DELAY = 1200;
- public static final float ALPHA_EXPANSION_THRESHOLD = 0.95f;
- /**
- * Values for the bouncer expansion represented as the panel expansion.
- * Panel expansion 1f = panel fully showing = bouncer fully hidden
- * Panel expansion 0f = panel fully hiding = bouncer fully showing
- */
- public static final float EXPANSION_HIDDEN = 1f;
- public static final float EXPANSION_VISIBLE = 0f;
-
- protected final Context mContext;
- protected final ViewMediatorCallback mCallback;
- protected final ViewGroup mContainer;
- private final FalsingCollector mFalsingCollector;
- private final DismissCallbackRegistry mDismissCallbackRegistry;
- private final Handler mHandler;
- private final List<PrimaryBouncerExpansionCallback> mExpansionCallbacks = new ArrayList<>();
- private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private final KeyguardStateController mKeyguardStateController;
- private final KeyguardSecurityModel mKeyguardSecurityModel;
- private final KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
- private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
- new KeyguardUpdateMonitorCallback() {
- @Override
- public void onStrongAuthStateChanged(int userId) {
- mBouncerPromptReason = mCallback.getBouncerPromptReason();
- }
-
- @Override
- public void onLockedOutStateChanged(BiometricSourceType type) {
- if (type == BiometricSourceType.FINGERPRINT) {
- mBouncerPromptReason = mCallback.getBouncerPromptReason();
- }
- }
-
- @Override
- public void onNonStrongBiometricAllowedChanged(int userId) {
- mBouncerPromptReason = mCallback.getBouncerPromptReason();
- }
- };
- private final Runnable mRemoveViewRunnable = this::removeView;
- private final KeyguardBypassController mKeyguardBypassController;
- private KeyguardHostViewController mKeyguardViewController;
- private final ListenerSet<KeyguardResetCallback> mResetCallbacks = new ListenerSet<>();
- private final Runnable mResetRunnable = ()-> {
- if (mKeyguardViewController != null) {
- mKeyguardViewController.resetSecurityContainer();
- for (KeyguardResetCallback callback : mResetCallbacks) {
- callback.onKeyguardReset();
- }
- }
- };
-
- private int mStatusBarHeight;
- private float mExpansion = EXPANSION_HIDDEN;
- private boolean mShowingSoon;
- private int mBouncerPromptReason;
- private boolean mIsAnimatingAway;
- private boolean mIsScrimmed;
- private boolean mInitialized;
-
- private KeyguardBouncer(Context context, ViewMediatorCallback callback,
- ViewGroup container,
- DismissCallbackRegistry dismissCallbackRegistry, FalsingCollector falsingCollector,
- PrimaryBouncerExpansionCallback expansionCallback,
- KeyguardStateController keyguardStateController,
- KeyguardUpdateMonitor keyguardUpdateMonitor,
- KeyguardBypassController keyguardBypassController, @Main Handler handler,
- KeyguardSecurityModel keyguardSecurityModel,
- KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory) {
- mContext = context;
- mCallback = callback;
- mContainer = container;
- mKeyguardUpdateMonitor = keyguardUpdateMonitor;
- mFalsingCollector = falsingCollector;
- mDismissCallbackRegistry = dismissCallbackRegistry;
- mHandler = handler;
- mKeyguardStateController = keyguardStateController;
- mKeyguardSecurityModel = keyguardSecurityModel;
- mKeyguardBouncerComponentFactory = keyguardBouncerComponentFactory;
- mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback);
- mKeyguardBypassController = keyguardBypassController;
- mExpansionCallbacks.add(expansionCallback);
- }
-
- /**
- * Get the KeyguardBouncer expansion
- * @return 1=HIDDEN, 0=SHOWING, in between 0 and 1 means the bouncer is in transition.
- */
- public float getExpansion() {
- return mExpansion;
- }
-
- /**
- * Enable/disable only the back button
- */
- public void setBackButtonEnabled(boolean enabled) {
- int vis = mContainer.getSystemUiVisibility();
- if (enabled) {
- vis &= ~View.STATUS_BAR_DISABLE_BACK;
- } else {
- vis |= View.STATUS_BAR_DISABLE_BACK;
- }
- mContainer.setSystemUiVisibility(vis);
- }
-
- public void show(boolean resetSecuritySelection) {
- show(resetSecuritySelection, true /* scrimmed */);
- }
-
- /**
- * Shows the bouncer.
- *
- * @param resetSecuritySelection Cleans keyguard view
- * @param isScrimmed true when the bouncer show show scrimmed, false when the user will be
- * dragging it and translation should be deferred.
- */
- public void show(boolean resetSecuritySelection, boolean isScrimmed) {
- final int keyguardUserId = KeyguardUpdateMonitor.getCurrentUser();
- if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
- // In split system user mode, we never unlock system user.
- return;
- }
-
- try {
- Trace.beginSection("KeyguardBouncer#show");
-
- ensureView();
- mIsScrimmed = isScrimmed;
-
- // On the keyguard, we want to show the bouncer when the user drags up, but it's
- // not correct to end the falsing session. We still need to verify if those touches
- // are valid.
- // Later, at the end of the animation, when the bouncer is at the top of the screen,
- // onFullyShown() will be called and FalsingManager will stop recording touches.
- if (isScrimmed) {
- setExpansion(EXPANSION_VISIBLE);
- }
-
- if (resetSecuritySelection) {
- // showPrimarySecurityScreen() updates the current security method. This is needed
- // in case we are already showing and the current security method changed.
- showPrimarySecurityScreen();
- }
-
- if (mContainer.getVisibility() == View.VISIBLE || mShowingSoon) {
- // Calls to reset must resume the ViewControllers when in fullscreen mode
- if (needsFullscreenBouncer()) {
- mKeyguardViewController.onResume();
- }
- return;
- }
-
- final int activeUserId = KeyguardUpdateMonitor.getCurrentUser();
- final boolean isSystemUser =
- UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM;
- final boolean allowDismissKeyguard = !isSystemUser && activeUserId == keyguardUserId;
-
- // If allowed, try to dismiss the Keyguard. If no security auth (password/pin/pattern)
- // is set, this will dismiss the whole Keyguard. Otherwise, show the bouncer.
- if (allowDismissKeyguard && mKeyguardViewController.dismiss(activeUserId)) {
- return;
- }
-
- // This condition may indicate an error on Android, so log it.
- if (!allowDismissKeyguard) {
- Log.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != "
- + keyguardUserId);
- }
-
- mShowingSoon = true;
-
- // Split up the work over multiple frames.
- DejankUtils.removeCallbacks(mResetRunnable);
- if (mKeyguardStateController.isFaceAuthEnabled()
- && !mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
- KeyguardUpdateMonitor.getCurrentUser())
- && !needsFullscreenBouncer()
- && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
- BiometricSourceType.FACE)
- && !mKeyguardBypassController.getBypassEnabled()) {
- mHandler.postDelayed(mShowRunnable, BOUNCER_FACE_DELAY);
- } else {
- DejankUtils.postAfterTraversal(mShowRunnable);
- }
-
- mKeyguardStateController.notifyBouncerShowing(true /* showing */);
- dispatchStartingToShow();
- } finally {
- Trace.endSection();
- }
- }
-
- public boolean isScrimmed() {
- return mIsScrimmed;
- }
-
- /**
- * This method must be called at the end of the bouncer animation when
- * the translation is performed manually by the user, otherwise FalsingManager
- * will never be notified and its internal state will be out of sync.
- */
- private void onFullyShown() {
- mFalsingCollector.onBouncerShown();
- if (mKeyguardViewController == null) {
- Log.e(TAG, "onFullyShown when view was null");
- } else {
- mKeyguardViewController.onResume();
- mContainer.announceForAccessibility(
- mKeyguardViewController.getAccessibilityTitleForCurrentMode());
- }
- }
-
- /**
- * @see #onFullyShown()
- */
- private void onFullyHidden() {
-
- }
-
- private void setVisibility(@View.Visibility int visibility) {
- mContainer.setVisibility(visibility);
- if (mKeyguardViewController != null) {
- mKeyguardViewController.onBouncerVisibilityChanged(visibility);
- }
- dispatchVisibilityChanged();
- }
-
- private final Runnable mShowRunnable = new Runnable() {
- @Override
- public void run() {
- setVisibility(View.VISIBLE);
- showPromptReason(mBouncerPromptReason);
- final CharSequence customMessage = mCallback.consumeCustomMessage();
- if (customMessage != null) {
- mKeyguardViewController.showErrorMessage(customMessage);
- }
- mKeyguardViewController.appear(mStatusBarHeight);
- mShowingSoon = false;
- if (mExpansion == EXPANSION_VISIBLE) {
- mKeyguardViewController.onResume();
- mKeyguardViewController.resetSecurityContainer();
- showPromptReason(mBouncerPromptReason);
- }
- }
- };
-
- /**
- * Show a string explaining why the security view needs to be solved.
- *
- * @param reason a flag indicating which string should be shown, see
- * {@link KeyguardSecurityView#PROMPT_REASON_NONE}
- * and {@link KeyguardSecurityView#PROMPT_REASON_RESTART}
- */
- public void showPromptReason(int reason) {
- if (mKeyguardViewController != null) {
- mKeyguardViewController.showPromptReason(reason);
- } else {
- Log.w(TAG, "Trying to show prompt reason on empty bouncer");
- }
- }
-
- public void showMessage(String message, ColorStateList colorState) {
- if (mKeyguardViewController != null) {
- mKeyguardViewController.showMessage(message, colorState);
- } else {
- Log.w(TAG, "Trying to show message on empty bouncer");
- }
- }
-
- private void cancelShowRunnable() {
- DejankUtils.removeCallbacks(mShowRunnable);
- mHandler.removeCallbacks(mShowRunnable);
- mShowingSoon = false;
- }
-
- public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) {
- ensureView();
- setDismissAction(r, cancelAction);
- show(false /* resetSecuritySelection */);
- }
-
- /**
- * Set the actions to run when the keyguard is dismissed or when the dismiss is cancelled. Those
- * actions will still be run even if this bouncer is not shown, for instance when authenticating
- * with an alternate authenticator like the UDFPS.
- */
- public void setDismissAction(OnDismissAction r, Runnable cancelAction) {
- mKeyguardViewController.setOnDismissAction(r, cancelAction);
- }
-
- public void hide(boolean destroyView) {
- Trace.beginSection("KeyguardBouncer#hide");
- if (isShowing()) {
- SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
- SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN);
- mDismissCallbackRegistry.notifyDismissCancelled();
- }
- mIsScrimmed = false;
- mFalsingCollector.onBouncerHidden();
- mKeyguardStateController.notifyBouncerShowing(false /* showing */);
- cancelShowRunnable();
- if (mKeyguardViewController != null) {
- mKeyguardViewController.cancelDismissAction();
- mKeyguardViewController.cleanUp();
- }
- mIsAnimatingAway = false;
- setVisibility(View.INVISIBLE);
- if (destroyView) {
-
- // We have a ViewFlipper that unregisters a broadcast when being detached, which may
- // be slow because of AM lock contention during unlocking. We can delay it a bit.
- mHandler.postDelayed(mRemoveViewRunnable, 50);
- }
- Trace.endSection();
- }
-
- /**
- * See {@link StatusBarKeyguardViewManager#startPreHideAnimation}.
- */
- public void startPreHideAnimation(Runnable runnable) {
- mIsAnimatingAway = true;
- if (mKeyguardViewController != null) {
- mKeyguardViewController.startDisappearAnimation(runnable);
- } else if (runnable != null) {
- runnable.run();
- }
- }
-
- /**
- * Reset the state of the view.
- */
- public void reset() {
- cancelShowRunnable();
- inflateView();
- mFalsingCollector.onBouncerHidden();
- }
-
- public void onScreenTurnedOff() {
- if (mKeyguardViewController != null && mContainer.getVisibility() == View.VISIBLE) {
- mKeyguardViewController.onPause();
- }
- }
-
- public boolean isShowing() {
- return (mShowingSoon || mContainer.getVisibility() == View.VISIBLE)
- && mExpansion == EXPANSION_VISIBLE && !isAnimatingAway();
- }
-
- /**
- * {@link #show(boolean)} was called but we're not showing yet, or being dragged.
- */
- public boolean inTransit() {
- return mShowingSoon || mExpansion != EXPANSION_HIDDEN && mExpansion != EXPANSION_VISIBLE;
- }
-
- /**
- * @return {@code true} when bouncer's pre-hide animation already started but isn't completely
- * hidden yet, {@code false} otherwise.
- */
- public boolean isAnimatingAway() {
- return mIsAnimatingAway;
- }
-
- public void prepare() {
- boolean wasInitialized = mInitialized;
- ensureView();
- if (wasInitialized) {
- showPrimarySecurityScreen();
- }
- mBouncerPromptReason = mCallback.getBouncerPromptReason();
- }
-
- private void showPrimarySecurityScreen() {
- mKeyguardViewController.showPrimarySecurityScreen();
- }
-
- /**
- * Current notification panel expansion
- * @param fraction 0 when notification panel is collapsed and 1 when expanded.
- * @see StatusBarKeyguardViewManager#onPanelExpansionChanged
- */
- public void setExpansion(float fraction) {
- float oldExpansion = mExpansion;
- boolean expansionChanged = mExpansion != fraction;
- mExpansion = fraction;
- if (mKeyguardViewController != null && !mIsAnimatingAway) {
- mKeyguardViewController.setExpansion(fraction);
- }
-
- if (fraction == EXPANSION_VISIBLE && oldExpansion != EXPANSION_VISIBLE) {
- onFullyShown();
- dispatchFullyShown();
- } else if (fraction == EXPANSION_HIDDEN && oldExpansion != EXPANSION_HIDDEN) {
- DejankUtils.postAfterTraversal(mResetRunnable);
- /*
- * There are cases where #hide() was not invoked, such as when
- * NotificationPanelViewController controls the hide animation. Make sure the state gets
- * updated by calling #hide() directly.
- */
- hide(false /* destroyView */);
- dispatchFullyHidden();
- } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) {
- dispatchStartingToHide();
- if (mKeyguardViewController != null) {
- mKeyguardViewController.onStartingToHide();
- }
- }
-
- if (expansionChanged) {
- dispatchExpansionChanged();
- }
- }
-
- public boolean willDismissWithAction() {
- return mKeyguardViewController != null && mKeyguardViewController.hasDismissActions();
- }
-
- public int getTop() {
- if (mKeyguardViewController == null) {
- return 0;
- }
-
- return mKeyguardViewController.getTop();
- }
-
- protected void ensureView() {
- // Removal of the view might be deferred to reduce unlock latency,
- // in this case we need to force the removal, otherwise we'll
- // end up in an unpredictable state.
- boolean forceRemoval = mHandler.hasCallbacks(mRemoveViewRunnable);
- if (!mInitialized || forceRemoval) {
- inflateView();
- }
- }
-
- protected void inflateView() {
- removeView();
- mHandler.removeCallbacks(mRemoveViewRunnable);
-
- KeyguardBouncerComponent component = mKeyguardBouncerComponentFactory.create(mContainer);
- mKeyguardViewController = component.getKeyguardHostViewController();
- mKeyguardViewController.init();
-
- mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
- setVisibility(View.INVISIBLE);
-
- final WindowInsets rootInsets = mContainer.getRootWindowInsets();
- if (rootInsets != null) {
- mContainer.dispatchApplyWindowInsets(rootInsets);
- }
- mInitialized = true;
- }
-
- protected void removeView() {
- mContainer.removeAllViews();
- mInitialized = false;
- }
-
- /**
- * @return True if and only if the security method should be shown before showing the
- * notifications on Keyguard, like SIM PIN/PUK.
- */
- public boolean needsFullscreenBouncer() {
- SecurityMode mode = mKeyguardSecurityModel.getSecurityMode(
- KeyguardUpdateMonitor.getCurrentUser());
- return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk;
- }
-
- /**
- * Like {@link #needsFullscreenBouncer}, but uses the currently visible security method, which
- * makes this method much faster.
- */
- public boolean isFullscreenBouncer() {
- if (mKeyguardViewController != null) {
- SecurityMode mode = mKeyguardViewController.getCurrentSecurityMode();
- return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk;
- }
- return false;
- }
-
- /**
- * WARNING: This method might cause Binder calls.
- */
- public boolean isSecure() {
- return mKeyguardSecurityModel.getSecurityMode(
- KeyguardUpdateMonitor.getCurrentUser()) != SecurityMode.None;
- }
-
- public boolean shouldDismissOnMenuPressed() {
- return mKeyguardViewController.shouldEnableMenuKey();
- }
-
- public boolean interceptMediaKey(KeyEvent event) {
- ensureView();
- return mKeyguardViewController.interceptMediaKey(event);
- }
-
- /**
- * @return true if the pre IME back event should be handled
- */
- public boolean dispatchBackKeyEventPreIme() {
- ensureView();
- return mKeyguardViewController.dispatchBackKeyEventPreIme();
- }
-
- public void notifyKeyguardAuthenticated(boolean strongAuth) {
- ensureView();
- mKeyguardViewController.finish(strongAuth, KeyguardUpdateMonitor.getCurrentUser());
- }
-
- private void dispatchFullyShown() {
- for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
- callback.onFullyShown();
- }
- }
-
- private void dispatchStartingToHide() {
- for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
- callback.onStartingToHide();
- }
- }
-
- private void dispatchStartingToShow() {
- for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
- callback.onStartingToShow();
- }
- }
-
- private void dispatchFullyHidden() {
- for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
- callback.onFullyHidden();
- }
- }
-
- private void dispatchExpansionChanged() {
- for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
- callback.onExpansionChanged(mExpansion);
- }
- }
-
- private void dispatchVisibilityChanged() {
- for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
- callback.onVisibilityChanged(mContainer.getVisibility() == View.VISIBLE);
- }
- }
-
- /**
- * Apply keyguard configuration from the currently active resources. This can be called when the
- * device configuration changes, to re-apply some resources that are qualified on the device
- * configuration.
- */
- public void updateResources() {
- if (mKeyguardViewController != null) {
- mKeyguardViewController.updateResources();
- }
- }
-
- public void dump(PrintWriter pw) {
- pw.println("KeyguardBouncer");
- pw.println(" isShowing(): " + isShowing());
- pw.println(" mStatusBarHeight: " + mStatusBarHeight);
- pw.println(" mExpansion: " + mExpansion);
- pw.println(" mKeyguardViewController; " + mKeyguardViewController);
- pw.println(" mShowingSoon: " + mShowingSoon);
- pw.println(" mBouncerPromptReason: " + mBouncerPromptReason);
- pw.println(" mIsAnimatingAway: " + mIsAnimatingAway);
- pw.println(" mInitialized: " + mInitialized);
- }
-
- /** Update keyguard position based on a tapped X coordinate. */
- public void updateKeyguardPosition(float x) {
- if (mKeyguardViewController != null) {
- mKeyguardViewController.updateKeyguardPosition(x);
- }
- }
-
- public void addKeyguardResetCallback(KeyguardResetCallback callback) {
- mResetCallbacks.addIfAbsent(callback);
- }
-
- public void removeKeyguardResetCallback(KeyguardResetCallback callback) {
- mResetCallbacks.remove(callback);
- }
-
- /**
- * Adds a callback to listen to bouncer expansion updates.
- */
- public void addBouncerExpansionCallback(PrimaryBouncerExpansionCallback callback) {
- if (!mExpansionCallbacks.contains(callback)) {
- mExpansionCallbacks.add(callback);
- }
- }
-
- /**
- * Removes a previously added callback. If the callback was never added, this methood
- * does nothing.
- */
- public void removeBouncerExpansionCallback(PrimaryBouncerExpansionCallback callback) {
- mExpansionCallbacks.remove(callback);
- }
-
- /**
- * Callback updated when the primary bouncer's show and hide states change.
- */
- public interface PrimaryBouncerExpansionCallback {
- /**
- * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_VISIBLE}.
- * This is NOT called each time the bouncer is shown, but rather only when the fully
- * shown amount has changed based on the panel expansion. The bouncer's visibility
- * can still change when the expansion amount hasn't changed.
- * See {@link KeyguardBouncer#isShowing()} for the checks for the bouncer showing state.
- */
- default void onFullyShown() {
- }
-
- /**
- * Invoked when the bouncer is starting to transition to a hidden state.
- */
- default void onStartingToHide() {
- }
-
- /**
- * Invoked when the bouncer is starting to transition to a visible state.
- */
- default void onStartingToShow() {
- }
-
- /**
- * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_HIDDEN}.
- */
- default void onFullyHidden() {
- }
-
- /**
- * From 0f {@link KeyguardBouncer#EXPANSION_VISIBLE} when fully visible
- * to 1f {@link KeyguardBouncer#EXPANSION_HIDDEN} when fully hidden
- */
- default void onExpansionChanged(float bouncerHideAmount) {}
-
- /**
- * Invoked when visibility of KeyguardBouncer has changed.
- * Note the bouncer expansion can be {@link KeyguardBouncer#EXPANSION_VISIBLE}, but the
- * view's visibility can be {@link View.INVISIBLE}.
- */
- default void onVisibilityChanged(boolean isVisible) {}
- }
-
- public interface KeyguardResetCallback {
- void onKeyguardReset();
- }
-
- /** Create a {@link KeyguardBouncer} once a container and bouncer callback are available. */
- public static class Factory {
- private final Context mContext;
- private final ViewMediatorCallback mCallback;
- private final DismissCallbackRegistry mDismissCallbackRegistry;
- private final FalsingCollector mFalsingCollector;
- private final KeyguardStateController mKeyguardStateController;
- private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private final KeyguardBypassController mKeyguardBypassController;
- private final Handler mHandler;
- private final KeyguardSecurityModel mKeyguardSecurityModel;
- private final KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
-
- @Inject
- public Factory(Context context, ViewMediatorCallback callback,
- DismissCallbackRegistry dismissCallbackRegistry, FalsingCollector falsingCollector,
- KeyguardStateController keyguardStateController,
- KeyguardUpdateMonitor keyguardUpdateMonitor,
- KeyguardBypassController keyguardBypassController, @Main Handler handler,
- KeyguardSecurityModel keyguardSecurityModel,
- KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory) {
- mContext = context;
- mCallback = callback;
- mDismissCallbackRegistry = dismissCallbackRegistry;
- mFalsingCollector = falsingCollector;
- mKeyguardStateController = keyguardStateController;
- mKeyguardUpdateMonitor = keyguardUpdateMonitor;
- mKeyguardBypassController = keyguardBypassController;
- mHandler = handler;
- mKeyguardSecurityModel = keyguardSecurityModel;
- mKeyguardBouncerComponentFactory = keyguardBouncerComponentFactory;
- }
-
- /**
- * Construct a KeyguardBouncer that will exist in the given container.
- */
- public KeyguardBouncer create(ViewGroup container,
- PrimaryBouncerExpansionCallback expansionCallback) {
- return new KeyguardBouncer(mContext, mCallback, container,
- mDismissCallbackRegistry, mFalsingCollector, expansionCallback,
- mKeyguardStateController, mKeyguardUpdateMonitor,
- mKeyguardBypassController, mHandler, mKeyguardSecurityModel,
- mKeyguardBouncerComponentFactory);
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index b965ac9..ff1b31d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -30,6 +30,9 @@
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.tuner.TunerService
import java.io.PrintWriter
@@ -40,11 +43,19 @@
private val mKeyguardStateController: KeyguardStateController
private val statusBarStateController: StatusBarStateController
+ private val devicePostureController: DevicePostureController
@BypassOverride private val bypassOverride: Int
private var hasFaceFeature: Boolean
+ @DevicePostureInt private val configFaceAuthSupportedPosture: Int
+ @DevicePostureInt private var postureState: Int = DEVICE_POSTURE_UNKNOWN
private var pendingUnlock: PendingUnlock? = null
private val listeners = mutableListOf<OnBypassStateChangedListener>()
-
+ private val postureCallback = DevicePostureController.Callback { posture ->
+ if (postureState != posture) {
+ postureState = posture
+ notifyListeners()
+ }
+ }
private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback {
override fun onFaceAuthEnabledChanged() = notifyListeners()
}
@@ -86,7 +97,8 @@
FACE_UNLOCK_BYPASS_NEVER -> false
else -> field
}
- return enabled && mKeyguardStateController.isFaceAuthEnabled
+ return enabled && mKeyguardStateController.isFaceAuthEnabled &&
+ isPostureAllowedForFaceAuth()
}
private set(value) {
field = value
@@ -106,18 +118,31 @@
lockscreenUserManager: NotificationLockscreenUserManager,
keyguardStateController: KeyguardStateController,
shadeExpansionStateManager: ShadeExpansionStateManager,
+ devicePostureController: DevicePostureController,
dumpManager: DumpManager
) {
this.mKeyguardStateController = keyguardStateController
this.statusBarStateController = statusBarStateController
+ this.devicePostureController = devicePostureController
bypassOverride = context.resources.getInteger(R.integer.config_face_unlock_bypass_override)
+ configFaceAuthSupportedPosture =
+ context.resources.getInteger(R.integer.config_face_auth_supported_posture)
- hasFaceFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)
+ hasFaceFeature = context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)
if (!hasFaceFeature) {
return
}
+ if (configFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) {
+ devicePostureController.addCallback { posture ->
+ if (postureState != posture) {
+ postureState = posture
+ notifyListeners()
+ }
+ }
+ }
+
dumpManager.registerDumpable("KeyguardBypassController", this)
statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
override fun onStateChanged(newState: Int) {
@@ -203,6 +228,13 @@
pendingUnlock = null
}
+ fun isPostureAllowedForFaceAuth(): Boolean {
+ return when (configFaceAuthSupportedPosture) {
+ DEVICE_POSTURE_UNKNOWN -> true
+ else -> (postureState == configFaceAuthSupportedPosture)
+ }
+ }
+
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("KeyguardBypassController:")
if (pendingUnlock != null) {
@@ -219,6 +251,7 @@
pw.println(" launchingAffordance: $launchingAffordance")
pw.println(" qSExpanded: $qsExpanded")
pw.println(" hasFaceFeature: $hasFaceFeature")
+ pw.println(" postureState: $postureState")
}
/** Registers a listener for bypass state changes. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 3483574..cba0897 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -45,6 +45,7 @@
import com.android.systemui.animation.Interpolators;
import com.android.systemui.battery.BatteryMeterViewController;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.plugins.log.LogLevel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.statusbar.CommandQueue;
@@ -76,6 +77,7 @@
/** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
+ private static final String TAG = "KeyguardStatusBarViewController";
private static final AnimationProperties KEYGUARD_HUN_PROPERTIES =
new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
@@ -355,7 +357,7 @@
mView.setOnApplyWindowInsetsListener(
(view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON),
+ Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
false,
mVolumeSettingObserver,
UserHandle.USER_ALL);
@@ -422,7 +424,7 @@
/** Animate the keyguard status bar in. */
public void animateKeyguardStatusBarIn() {
- mLogger.d("animating status bar in");
+ mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in");
if (mDisableStateTracker.isDisabled()) {
// If our view is disabled, don't allow us to animate in.
return;
@@ -438,7 +440,7 @@
/** Animate the keyguard status bar out. */
public void animateKeyguardStatusBarOut(long startDelay, long duration) {
- mLogger.d("animating status bar out");
+ mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out");
ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f);
anim.addUpdateListener(mAnimatorUpdateListener);
anim.setStartDelay(startDelay);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index 4d14542..fe2a913 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -38,6 +37,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.statusbar.policy.BatteryController;
import java.io.PrintWriter;
@@ -94,7 +94,8 @@
DarkIconDispatcher darkIconDispatcher,
BatteryController batteryController,
NavigationModeController navModeController,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ DisplayTracker displayTracker) {
mDarkIconColor = ctx.getColor(R.color.dark_mode_icon_color_single_tone);
mLightIconColor = ctx.getColor(R.color.light_mode_icon_color_single_tone);
mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
@@ -104,7 +105,7 @@
mNavigationMode = mode;
});
- if (ctx.getDisplayId() == DEFAULT_DISPLAY) {
+ if (ctx.getDisplayId() == displayTracker.getDefaultDisplayId()) {
dumpManager.registerDumpable(getClass().getSimpleName(), this);
}
}
@@ -317,24 +318,27 @@
private final BatteryController mBatteryController;
private final NavigationModeController mNavModeController;
private final DumpManager mDumpManager;
+ private final DisplayTracker mDisplayTracker;
@Inject
public Factory(
DarkIconDispatcher darkIconDispatcher,
BatteryController batteryController,
NavigationModeController navModeController,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ DisplayTracker displayTracker) {
mDarkIconDispatcher = darkIconDispatcher;
mBatteryController = batteryController;
mNavModeController = navModeController;
mDumpManager = dumpManager;
+ mDisplayTracker = displayTracker;
}
/** Create an {@link LightBarController} */
public LightBarController create(Context context) {
return new LightBarController(context, mDarkIconDispatcher, mBatteryController,
- mNavModeController, mDumpManager);
+ mNavModeController, mDumpManager, mDisplayTracker);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 48e58fc..6c532a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -35,7 +35,6 @@
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings.Global;
import android.service.notification.ZenModeConfig;
@@ -58,6 +57,7 @@
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.BluetoothController;
import com.android.systemui.statusbar.policy.CastController;
@@ -136,6 +136,7 @@
private final UserInfoController mUserInfoController;
private final IActivityManager mIActivityManager;
private final UserManager mUserManager;
+ private final UserTracker mUserTracker;
private final DevicePolicyManager mDevicePolicyManager;
private final StatusBarIconController mIconController;
private final CommandQueue mCommandQueue;
@@ -176,7 +177,7 @@
KeyguardStateController keyguardStateController,
LocationController locationController,
SensorPrivacyController sensorPrivacyController, IActivityManager iActivityManager,
- AlarmManager alarmManager, UserManager userManager,
+ AlarmManager alarmManager, UserManager userManager, UserTracker userTracker,
DevicePolicyManager devicePolicyManager, RecordingController recordingController,
@Nullable TelecomManager telecomManager, @DisplayId int displayId,
@Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
@@ -196,6 +197,7 @@
mUserInfoController = userInfoController;
mIActivityManager = iActivityManager;
mUserManager = userManager;
+ mUserTracker = userTracker;
mDevicePolicyManager = devicePolicyManager;
mRotationLockController = rotationLockController;
mDataSaver = dataSaverController;
@@ -366,7 +368,7 @@
}
private void updateAlarm() {
- final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT);
+ final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(mUserTracker.getUserId());
final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0;
int zen = mZenController.getZen();
final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index ee8b861..80093a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -53,6 +53,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -147,7 +148,7 @@
* 0, the bouncer is visible.
*/
@FloatRange(from = 0, to = 1)
- private float mBouncerHiddenFraction = KeyguardBouncer.EXPANSION_HIDDEN;
+ private float mBouncerHiddenFraction = KeyguardBouncerConstants.EXPANSION_HIDDEN;
/**
* Set whether an unocclusion animation is currently running on the notification panel. Used
@@ -790,27 +791,35 @@
if (!mScreenOffAnimationController.shouldExpandNotifications()
&& !mAnimatingPanelExpansionOnUnlock
&& !occluding) {
- float behindFraction = getInterpolatedFraction();
- behindFraction = (float) Math.pow(behindFraction, 0.8f);
if (mClipsQsScrim) {
+ float behindFraction = getInterpolatedFraction();
+ behindFraction = (float) Math.pow(behindFraction, 0.8f);
mBehindAlpha = mTransparentScrimBackground ? 0 : 1;
mNotificationsAlpha =
mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
} else {
- mBehindAlpha =
- mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
- // Delay fade-in of notification scrim a bit further, to coincide with the
- // view fade in. Otherwise the empty panel can be quite jarring.
- mNotificationsAlpha = mTransparentScrimBackground
- ? 0 : MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f,
- mPanelExpansionFraction);
+ if (mTransparentScrimBackground) {
+ mBehindAlpha = 0;
+ mNotificationsAlpha = 0;
+ } else {
+ // Behind scrim will finish fading in at 30% expansion.
+ float behindFraction = MathUtils
+ .constrainedMap(0f, 1f, 0f, 0.3f, mPanelExpansionFraction);
+ mBehindAlpha = behindFraction * mDefaultScrimAlpha;
+ // Delay fade-in of notification scrim a bit further, to coincide with the
+ // behind scrim finishing fading in.
+ // Also to coincide with the view starting to fade in, otherwise the empty
+ // panel can be quite jarring.
+ mNotificationsAlpha = MathUtils
+ .constrainedMap(0f, 1f, 0.3f, 0.75f, mPanelExpansionFraction);
+ }
}
mBehindTint = mState.getBehindTint();
mInFrontAlpha = 0;
}
if (mState == ScrimState.DREAMING
- && mBouncerHiddenFraction != KeyguardBouncer.EXPANSION_HIDDEN) {
+ && mBouncerHiddenFraction != KeyguardBouncerConstants.EXPANSION_HIDDEN) {
final float interpolatedFraction =
BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(
mBouncerHiddenFraction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 1a14a036..11863627 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -79,12 +79,30 @@
/** Refresh the state of an IconManager by recreating the views */
void refreshIconGroup(IconManager iconManager);
- /** */
+
+ /**
+ * Adds or updates an icon for a given slot for a **tile service icon**.
+ *
+ * TODO(b/265307726): Merge with {@link #setIcon(String, StatusBarIcon)} or make this method
+ * much more clearly distinct from that method.
+ */
void setExternalIcon(String slot);
- /** */
+
+ /**
+ * Adds or updates an icon for the given slot for **internal system icons**.
+ *
+ * TODO(b/265307726): Rename to `setInternalIcon`, or merge this appropriately with the
+ * {@link #setIcon(String, StatusBarIcon)} method.
+ */
void setIcon(String slot, int resourceId, CharSequence contentDescription);
- /** */
+
+ /**
+ * Adds or updates an icon for the given slot for an **externally-provided icon**.
+ *
+ * TODO(b/265307726): Rename to `setExternalIcon` or something similar.
+ */
void setIcon(String slot, StatusBarIcon icon);
+
/** */
void setWifiIcon(String slot, WifiIconState state);
@@ -133,9 +151,17 @@
* TAG_PRIMARY to refer to the first icon at a given slot.
*/
void removeIcon(String slot, int tag);
+
/** */
void removeAllIconsForSlot(String slot);
+ /**
+ * Removes all the icons for the given slot.
+ *
+ * Only use this for icons that have come from **an external process**.
+ */
+ void removeAllIconsForExternalSlot(String slot);
+
// TODO: See if we can rename this tunable name.
String ICON_HIDE_LIST = "icon_blacklist";
@@ -475,7 +501,7 @@
@VisibleForTesting
protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
if (mStatusBarPipelineFlags.useNewWifiIcon()) {
- throw new IllegalStateException("Attempting to add a mobile icon while the new "
+ throw new IllegalStateException("Attempting to add a wifi icon while the new "
+ "icons are enabled is not supported");
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 9fbe6cb..416bc71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -28,6 +28,8 @@
import android.util.Log;
import android.view.ViewGroup;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
@@ -63,6 +65,10 @@
ConfigurationListener, Dumpable, CommandQueue.Callbacks, StatusBarIconController, DemoMode {
private static final String TAG = "StatusBarIconController";
+ // Use this suffix to prevent external icon slot names from unintentionally overriding our
+ // internal, system-level slot names. See b/255428281.
+ @VisibleForTesting
+ protected static final String EXTERNAL_SLOT_SUFFIX = "__external";
private final StatusBarIconList mStatusBarIconList;
private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
@@ -346,21 +352,26 @@
@Override
public void setExternalIcon(String slot) {
- int viewIndex = mStatusBarIconList.getViewIndex(slot, 0);
+ String slotName = createExternalSlotName(slot);
+ int viewIndex = mStatusBarIconList.getViewIndex(slotName, 0);
int height = mContext.getResources().getDimensionPixelSize(
R.dimen.status_bar_icon_drawing_size);
mIconGroups.forEach(l -> l.onIconExternal(viewIndex, height));
}
- //TODO: remove this (used in command queue and for 3rd party tiles?)
+ // Override for *both* CommandQueue.Callbacks AND StatusBarIconController.
+ // TODO(b/265307726): Pull out the CommandQueue callbacks into a member variable to
+ // differentiate between those callback methods and StatusBarIconController methods.
+ @Override
public void setIcon(String slot, StatusBarIcon icon) {
+ String slotName = createExternalSlotName(slot);
if (icon == null) {
- removeAllIconsForSlot(slot);
+ removeAllIconsForSlot(slotName);
return;
}
StatusBarIconHolder holder = StatusBarIconHolder.fromIcon(icon);
- setIcon(slot, holder);
+ setIcon(slotName, holder);
}
private void setIcon(String slot, @NonNull StatusBarIconHolder holder) {
@@ -406,10 +417,12 @@
}
}
- /** */
+ // CommandQueue.Callbacks override
+ // TODO(b/265307726): Pull out the CommandQueue callbacks into a member variable to
+ // differentiate between those callback methods and StatusBarIconController methods.
@Override
public void removeIcon(String slot) {
- removeAllIconsForSlot(slot);
+ removeAllIconsForExternalSlot(slot);
}
/** */
@@ -423,6 +436,11 @@
mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
}
+ @Override
+ public void removeAllIconsForExternalSlot(String slotName) {
+ removeAllIconsForSlot(createExternalSlotName(slotName));
+ }
+
/** */
@Override
public void removeAllIconsForSlot(String slotName) {
@@ -506,4 +524,12 @@
public void onDensityOrFontScaleChanged() {
refreshIconGroups();
}
+
+ private String createExternalSlotName(String slot) {
+ if (slot.endsWith(EXTERNAL_SLOT_SUFFIX)) {
+ return slot;
+ } else {
+ return slot + EXTERNAL_SLOT_SUFFIX;
+ }
+ }
}
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 d480fab..fd46571 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,6 +18,7 @@
import static android.view.WindowInsets.Type.navigationBars;
+import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
@@ -36,7 +37,8 @@
import android.view.ViewGroup;
import android.view.ViewRootImpl;
import android.view.WindowManagerGlobal;
-import android.window.OnBackInvokedCallback;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
@@ -58,7 +60,9 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.data.BouncerView;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -75,7 +79,6 @@
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.unfold.FoldAodAnimationController;
@@ -126,7 +129,6 @@
private final ConfigurationController mConfigurationController;
private final NavigationModeController mNavigationModeController;
private final NotificationShadeWindowController mNotificationShadeWindowController;
- private final KeyguardBouncer.Factory mKeyguardBouncerFactory;
private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
private final DreamOverlayStateController mDreamOverlayStateController;
@Nullable
@@ -134,6 +136,7 @@
private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController;
private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+ private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final BouncerView mPrimaryBouncerView;
private final Lazy<ShadeController> mShadeController;
@@ -182,8 +185,7 @@
isVisible && mDreamOverlayStateController.isOverlayActive());
if (!isVisible) {
- mCentralSurfaces.setPrimaryBouncerHiddenFraction(
- KeyguardBouncer.EXPANSION_HIDDEN);
+ mCentralSurfaces.setPrimaryBouncerHiddenFraction(EXPANSION_HIDDEN);
}
/* Register predictive back callback when keyguard becomes visible, and unregister
@@ -196,11 +198,38 @@
}
};
- private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
- if (DEBUG) {
- Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()");
+ private final OnBackAnimationCallback mOnBackInvokedCallback = new OnBackAnimationCallback() {
+ @Override
+ public void onBackInvoked() {
+ if (DEBUG) {
+ Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()");
+ }
+ onBackPressed();
+ if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+ mPrimaryBouncerView.getDelegate().getBackCallback().onBackInvoked();
+ }
}
- onBackPressed();
+
+ @Override
+ public void onBackProgressed(BackEvent event) {
+ if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+ mPrimaryBouncerView.getDelegate().getBackCallback().onBackProgressed(event);
+ }
+ }
+
+ @Override
+ public void onBackCancelled() {
+ if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+ mPrimaryBouncerView.getDelegate().getBackCallback().onBackCancelled();
+ }
+ }
+
+ @Override
+ public void onBackStarted(BackEvent event) {
+ if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
+ mPrimaryBouncerView.getDelegate().getBackCallback().onBackStarted(event);
+ }
+ }
};
private boolean mIsBackCallbackRegistered = false;
@@ -226,7 +255,6 @@
private View mNotificationContainer;
- @Nullable protected KeyguardBouncer mPrimaryBouncer;
protected boolean mRemoteInputActive;
private boolean mGlobalActionsVisible = false;
private boolean mLastGlobalActionsVisible = false;
@@ -251,8 +279,9 @@
private boolean mLastScreenOffAnimationPlaying;
private float mQsExpansion;
final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
- private boolean mIsModernBouncerEnabled;
private boolean mIsUnoccludeTransitionFlagEnabled;
+ private boolean mIsModernAlternateBouncerEnabled;
+ private boolean mIsBackAnimationEnabled;
private OnDismissAction mAfterKeyguardGoneAction;
private Runnable mKeyguardGoneCancelAction;
@@ -269,7 +298,7 @@
private final LatencyTracker mLatencyTracker;
private final KeyguardSecurityModel mKeyguardSecurityModel;
@Nullable private KeyguardBypassController mBypassController;
- @Nullable private AlternateBouncer mAlternateBouncer;
+ @Nullable private OccludingAppBiometricUI mOccludingAppBiometricUI;
private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
new KeyguardUpdateMonitorCallback() {
@@ -297,7 +326,6 @@
NotificationShadeWindowController notificationShadeWindowController,
KeyguardStateController keyguardStateController,
NotificationMediaManager notificationMediaManager,
- KeyguardBouncer.Factory keyguardBouncerFactory,
KeyguardMessageAreaController.Factory keyguardMessageAreaFactory,
Optional<SysUIUnfoldComponent> sysUIUnfoldComponent,
Lazy<ShadeController> shadeController,
@@ -306,7 +334,8 @@
FeatureFlags featureFlags,
PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
PrimaryBouncerInteractor primaryBouncerInteractor,
- BouncerView primaryBouncerView) {
+ BouncerView primaryBouncerView,
+ AlternateBouncerInteractor alternateBouncerInteractor) {
mContext = context;
mViewMediatorCallback = callback;
mLockPatternUtils = lockPatternUtils;
@@ -319,7 +348,6 @@
mKeyguardUpdateManager = keyguardUpdateMonitor;
mStatusBarStateController = sysuiStatusBarStateController;
mDockManager = dockManager;
- mKeyguardBouncerFactory = keyguardBouncerFactory;
mKeyguardMessageAreaFactory = keyguardMessageAreaFactory;
mShadeController = shadeController;
mLatencyTracker = latencyTracker;
@@ -329,8 +357,11 @@
mPrimaryBouncerView = primaryBouncerView;
mFoldAodAnimationController = sysUIUnfoldComponent
.map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
- mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+ mIsModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER);
+ mAlternateBouncerInteractor = alternateBouncerInteractor;
+ mIsBackAnimationEnabled =
+ featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM);
}
@Override
@@ -344,11 +375,7 @@
mBiometricUnlockController = biometricUnlockController;
ViewGroup container = mCentralSurfaces.getBouncerContainer();
- if (mIsModernBouncerEnabled) {
- mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
- } else {
- mPrimaryBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
- }
+ mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
mNotificationPanelViewController = notificationPanelViewController;
if (shadeExpansionStateManager != null) {
shadeExpansionStateManager.addExpansionListener(this);
@@ -363,23 +390,51 @@
}
/**
- * Sets the given alt auth interceptor to null if it's the current auth interceptor. Else,
- * does nothing.
+ * Sets the given legacy alternate bouncer to null if it's the current alternate bouncer. Else,
+ * does nothing. Only used if modern alternate bouncer is NOT enabled.
*/
- public void removeAlternateAuthInterceptor(@NonNull AlternateBouncer authInterceptor) {
- if (Objects.equals(mAlternateBouncer, authInterceptor)) {
- mAlternateBouncer = null;
- hideAlternateBouncer(true);
+ public void removeLegacyAlternateBouncer(
+ @NonNull LegacyAlternateBouncer alternateBouncerLegacy) {
+ if (!mIsModernAlternateBouncerEnabled) {
+ if (Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(),
+ alternateBouncerLegacy)) {
+ mAlternateBouncerInteractor.setLegacyAlternateBouncer(null);
+ hideAlternateBouncer(true);
+ }
}
}
/**
- * Sets a new alt auth interceptor.
+ * Sets a new legacy alternate bouncer. Only used if mdoern alternate bouncer is NOT enable.
*/
- public void setAlternateBouncer(@NonNull AlternateBouncer authInterceptor) {
- if (!Objects.equals(mAlternateBouncer, authInterceptor)) {
- mAlternateBouncer = authInterceptor;
- hideAlternateBouncer(false);
+ public void setLegacyAlternateBouncer(@NonNull LegacyAlternateBouncer alternateBouncerLegacy) {
+ if (!mIsModernAlternateBouncerEnabled) {
+ if (!Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(),
+ alternateBouncerLegacy)) {
+ mAlternateBouncerInteractor.setLegacyAlternateBouncer(alternateBouncerLegacy);
+ hideAlternateBouncer(false);
+ }
+ }
+
+ }
+
+
+ /**
+ * Sets the given OccludingAppBiometricUI to null if it's the current auth interceptor. Else,
+ * does nothing.
+ */
+ public void removeOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) {
+ if (Objects.equals(mOccludingAppBiometricUI, biometricUI)) {
+ mOccludingAppBiometricUI = null;
+ }
+ }
+
+ /**
+ * Sets a new OccludingAppBiometricUI.
+ */
+ public void setOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) {
+ if (!Objects.equals(mOccludingAppBiometricUI, biometricUI)) {
+ mOccludingAppBiometricUI = biometricUI;
}
}
@@ -438,6 +493,11 @@
}
}
+ private boolean shouldPlayBackAnimation() {
+ // Suppress back animation when bouncer shouldn't be dismissed on back invocation.
+ return !needsFullscreenBouncer() && mIsBackAnimationEnabled;
+ }
+
@Override
public void onDensityOrFontScaleChanged() {
hideBouncer(true /* destroyView */);
@@ -451,7 +511,7 @@
|| mNotificationPanelViewController.isExpanding());
final boolean isUserTrackingStarted =
- event.getFraction() != KeyguardBouncer.EXPANSION_HIDDEN && event.getTracking();
+ event.getFraction() != EXPANSION_HIDDEN && event.getTracking();
return mKeyguardStateController.isShowing()
&& !primaryBouncerIsOrWillBeShowing()
@@ -482,11 +542,7 @@
* show if any subsequent events are to be handled.
*/
if (beginShowingBouncer(event)) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
- } else {
- mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
- }
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
}
if (!primaryBouncerIsOrWillBeShowing()) {
@@ -494,17 +550,9 @@
}
if (mKeyguardStateController.isShowing()) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.setExpansion(fraction);
- } else {
- mPrimaryBouncerInteractor.setPanelExpansion(fraction);
- }
+ mPrimaryBouncerInteractor.setPanelExpansion(fraction);
} else {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
- } else {
- mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
- }
+ mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN);
}
}
@@ -534,24 +582,17 @@
/**
* Shows the notification keyguard or the bouncer depending on
- * {@link KeyguardBouncer#needsFullscreenBouncer()}.
+ * {@link #needsFullscreenBouncer()}.
*/
protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
if (needsFullscreenBouncer() && !mDozing) {
// The keyguard might be showing (already). So we need to hide it.
mCentralSurfaces.hideKeyguard();
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.show(true /* resetSecuritySelection */);
- } else {
- mPrimaryBouncerInteractor.show(true);
- }
+ mPrimaryBouncerInteractor.show(true);
} else {
mCentralSurfaces.showKeyguard();
if (hideBouncerWhenShowing) {
hideBouncer(false /* destroyView */);
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.prepare();
- }
}
}
updateStates();
@@ -566,18 +607,11 @@
* {@see KeyguardBouncer#show(boolean, boolean)}
*/
public void showBouncer(boolean scrimmed) {
- if (canShowAlternateBouncer()) {
- updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer());
- return;
+ if (!mAlternateBouncerInteractor.show()) {
+ showPrimaryBouncer(scrimmed);
+ } else {
+ updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
}
-
- showPrimaryBouncer(scrimmed);
- }
-
- /** Whether we can show the alternate bouncer instead of the primary bouncer. */
- public boolean canShowAlternateBouncer() {
- return mAlternateBouncer != null
- && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true);
}
/**
@@ -585,11 +619,7 @@
*/
@VisibleForTesting
void hideBouncer(boolean destroyView) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.hide(destroyView);
- } else {
- mPrimaryBouncerInteractor.hide();
- }
+ mPrimaryBouncerInteractor.hide();
if (mKeyguardStateController.isShowing()) {
// If we were showing the bouncer and then aborting, we need to also clear out any
// potential actions unless we actually unlocked.
@@ -608,11 +638,7 @@
hideAlternateBouncer(false);
if (mKeyguardStateController.isShowing() && !isBouncerShowing()) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.show(false /* resetSecuritySelection */, scrimmed);
- } else {
- mPrimaryBouncerInteractor.show(scrimmed);
- }
+ mPrimaryBouncerInteractor.show(scrimmed);
}
updateStates();
}
@@ -641,44 +667,30 @@
mKeyguardGoneCancelAction = cancelAction;
mDismissActionWillAnimateOnKeyguard = r != null && r.willRunAnimationOnKeyguard();
- // If there is an an alternate auth interceptor (like the UDFPS), show that one
+ // If there is an alternate auth interceptor (like the UDFPS), show that one
// instead of the bouncer.
- if (canShowAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) {
if (!afterKeyguardGone) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.setDismissAction(mAfterKeyguardGoneAction,
- mKeyguardGoneCancelAction);
- } else {
- mPrimaryBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
- mKeyguardGoneCancelAction);
- }
+ mPrimaryBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
+ mKeyguardGoneCancelAction);
mAfterKeyguardGoneAction = null;
mKeyguardGoneCancelAction = null;
}
- updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer());
+ updateAlternateBouncerShowing(mAlternateBouncerInteractor.show());
return;
}
if (afterKeyguardGone) {
// we'll handle the dismiss action after keyguard is gone, so just show the
// bouncer
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.show(false /* resetSecuritySelection */);
- } else {
- mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
- }
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
} else {
// after authentication success, run dismiss action with the option to defer
// hiding the keyguard based on the return value of the OnDismissAction
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.showWithDismissAction(mAfterKeyguardGoneAction,
- mKeyguardGoneCancelAction);
- } else {
- mPrimaryBouncerInteractor.setDismissAction(
- mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
- mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
- }
+ mPrimaryBouncerInteractor.setDismissAction(
+ mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
// bouncer will handle the dismiss action, so we no longer need to track it here
mAfterKeyguardGoneAction = null;
mKeyguardGoneCancelAction = null;
@@ -725,10 +737,7 @@
@Override
public void hideAlternateBouncer(boolean forceUpdateScrim) {
- final boolean updateScrim = (mAlternateBouncer != null
- && mAlternateBouncer.hideAlternateBouncer())
- || forceUpdateScrim;
- updateAlternateBouncerShowing(updateScrim);
+ updateAlternateBouncerShowing(mAlternateBouncerInteractor.hide() || forceUpdateScrim);
}
private void updateAlternateBouncerShowing(boolean updateScrim) {
@@ -738,7 +747,7 @@
return;
}
- final boolean isShowingAlternateBouncer = isShowingAlternateBouncer();
+ final boolean isShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState();
if (mKeyguardMessageAreaController != null) {
mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer);
mKeyguardMessageAreaController.setMessage("");
@@ -781,11 +790,7 @@
@Override
public void onFinishedGoingToSleep() {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.onScreenTurnedOff();
- } else {
- mPrimaryBouncerInteractor.onScreenTurnedOff();
- }
+ mPrimaryBouncerInteractor.onScreenTurnedOff();
}
@Override
@@ -879,11 +884,7 @@
@Override
public void startPreHideAnimation(Runnable finishRunnable) {
if (primaryBouncerIsShowing()) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.startPreHideAnimation(finishRunnable);
- } else {
- mPrimaryBouncerInteractor.startDisappearAnimation(finishRunnable);
- }
+ mPrimaryBouncerInteractor.startDisappearAnimation(finishRunnable);
mNotificationPanelViewController.startBouncerPreHideAnimation();
// We update the state (which will show the keyguard) only if an animation will run on
@@ -991,17 +992,7 @@
}
public void onThemeChanged() {
- if (mIsModernBouncerEnabled) {
- updateResources();
- return;
- }
- boolean wasShowing = primaryBouncerIsShowing();
- boolean wasScrimmed = primaryBouncerIsScrimmed();
-
- hideBouncer(true /* destroyView */);
- mPrimaryBouncer.prepare();
-
- if (wasShowing) showPrimaryBouncer(wasScrimmed);
+ updateResources();
}
public void onKeyguardFadedAway() {
@@ -1046,10 +1037,6 @@
* WARNING: This method might cause Binder calls.
*/
public boolean isSecure() {
- if (mPrimaryBouncer != null) {
- return mPrimaryBouncer.isSecure();
- }
-
return mKeyguardSecurityModel.getSecurityMode(
KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None;
}
@@ -1087,7 +1074,7 @@
if (hideImmediately) {
mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
} else {
- mNotificationPanelViewController.expandWithoutQs();
+ mNotificationPanelViewController.expandShadeToNotifications();
}
}
return;
@@ -1095,7 +1082,7 @@
@Override
public boolean isBouncerShowing() {
- return primaryBouncerIsShowing() || isShowingAlternateBouncer();
+ return primaryBouncerIsShowing() || mAlternateBouncerInteractor.isVisibleState();
}
@Override
@@ -1104,10 +1091,8 @@
}
public boolean isFullscreenBouncer() {
- if (mPrimaryBouncerView.getDelegate() != null) {
- return mPrimaryBouncerView.getDelegate().isFullScreenBouncer();
- }
- return mPrimaryBouncer != null && mPrimaryBouncer.isFullscreenBouncer();
+ return mPrimaryBouncerView.getDelegate() != null
+ && mPrimaryBouncerView.getDelegate().isFullScreenBouncer();
}
/**
@@ -1163,17 +1148,9 @@
!= (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
|| mFirstUpdate) {
if (primaryBouncerDismissible || !showing || remoteInputActive) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.setBackButtonEnabled(true);
- } else {
- mPrimaryBouncerInteractor.setBackButtonEnabled(true);
- }
+ mPrimaryBouncerInteractor.setBackButtonEnabled(true);
} else {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.setBackButtonEnabled(false);
- } else {
- mPrimaryBouncerInteractor.setBackButtonEnabled(false);
- }
+ mPrimaryBouncerInteractor.setBackButtonEnabled(false);
}
}
@@ -1267,27 +1244,21 @@
}
public boolean shouldDismissOnMenuPressed() {
- if (mPrimaryBouncerView.getDelegate() != null) {
- return mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
- }
- return mPrimaryBouncer != null && mPrimaryBouncer.shouldDismissOnMenuPressed();
+ return mPrimaryBouncerView.getDelegate() != null
+ && mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
}
public boolean interceptMediaKey(KeyEvent event) {
- if (mPrimaryBouncerView.getDelegate() != null) {
- return mPrimaryBouncerView.getDelegate().interceptMediaKey(event);
- }
- return mPrimaryBouncer != null && mPrimaryBouncer.interceptMediaKey(event);
+ return mPrimaryBouncerView.getDelegate() != null
+ && mPrimaryBouncerView.getDelegate().interceptMediaKey(event);
}
/**
* @return true if the pre IME back event should be handled
*/
public boolean dispatchBackKeyEventPreIme() {
- if (mPrimaryBouncerView.getDelegate() != null) {
- return mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme();
- }
- return mPrimaryBouncer != null && mPrimaryBouncer.dispatchBackKeyEventPreIme();
+ return mPrimaryBouncerView.getDelegate() != null
+ && mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme();
}
public void readyForKeyguardDone() {
@@ -1333,13 +1304,9 @@
* fingerprint.
*/
public void notifyKeyguardAuthenticated(boolean strongAuth) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.notifyKeyguardAuthenticated(strongAuth);
- } else {
- mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
- }
+ mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
- if (mAlternateBouncer != null && isShowingAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.isVisibleState()) {
hideAlternateBouncer(false);
executeAfterKeyguardGoneAction();
}
@@ -1347,16 +1314,12 @@
/** Display security message to relevant KeyguardMessageArea. */
public void setKeyguardMessage(String message, ColorStateList colorState) {
- if (isShowingAlternateBouncer()) {
+ if (mAlternateBouncerInteractor.isVisibleState()) {
if (mKeyguardMessageAreaController != null) {
mKeyguardMessageAreaController.setMessage(message);
}
} else {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.showMessage(message, colorState);
- } else {
- mPrimaryBouncerInteractor.showMessage(message, colorState);
- }
+ mPrimaryBouncerInteractor.showMessage(message, colorState);
}
}
@@ -1412,15 +1375,12 @@
* configuration.
*/
public void updateResources() {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.updateResources();
- } else {
- mPrimaryBouncerInteractor.updateResources();
- }
+ mPrimaryBouncerInteractor.updateResources();
}
public void dump(PrintWriter pw) {
pw.println("StatusBarKeyguardViewManager:");
+ pw.println(" mIsModernAlternateBouncerEnabled: " + mIsModernAlternateBouncerEnabled);
pw.println(" mRemoteInputActive: " + mRemoteInputActive);
pw.println(" mDozing: " + mDozing);
pw.println(" mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction);
@@ -1433,14 +1393,9 @@
pw.println(" " + callback);
}
- if (mPrimaryBouncer != null) {
- pw.println("PrimaryBouncer:");
- mPrimaryBouncer.dump(pw);
- }
-
- if (mAlternateBouncer != null) {
- pw.println("AlternateBouncer:");
- mAlternateBouncer.dump(pw);
+ if (mOccludingAppBiometricUI != null) {
+ pw.println("mOccludingAppBiometricUI:");
+ mOccludingAppBiometricUI.dump(pw);
}
}
@@ -1487,19 +1442,17 @@
}
}
- @Nullable
- public KeyguardBouncer getPrimaryBouncer() {
- return mPrimaryBouncer;
- }
-
- public boolean isShowingAlternateBouncer() {
- return mAlternateBouncer != null && mAlternateBouncer.isShowingAlternateBouncer();
- }
-
/**
- * Forward touches to callbacks.
+ * For any touches on the NPVC, show the primary bouncer if the alternate bouncer is currently
+ * showing.
*/
public void onTouch(MotionEvent event) {
+ if (mAlternateBouncerInteractor.isVisibleState()
+ && mAlternateBouncerInteractor.hasAlternateBouncerShownWithMinTime()) {
+ showPrimaryBouncer(true);
+ }
+
+ // Forward NPVC touches to callbacks in case they want to respond to touches
for (KeyguardViewManagerCallback callback: mCallbacks) {
callback.onTouch(event);
}
@@ -1507,11 +1460,7 @@
/** Update keyguard position based on a tapped X coordinate. */
public void updateKeyguardPosition(float x) {
- if (mPrimaryBouncer != null) {
- mPrimaryBouncer.updateKeyguardPosition(x);
- } else {
- mPrimaryBouncerInteractor.setKeyguardPosition(x);
- }
+ mPrimaryBouncerInteractor.setKeyguardPosition(x);
}
private static class DismissWithActionRequest {
@@ -1542,8 +1491,8 @@
*/
public void requestFp(boolean request, int udfpsColor) {
mKeyguardUpdateManager.requestFingerprintAuthOnOccludingApp(request);
- if (mAlternateBouncer != null) {
- mAlternateBouncer.requestUdfps(request, udfpsColor);
+ if (mOccludingAppBiometricUI != null) {
+ mOccludingAppBiometricUI.requestUdfps(request, udfpsColor);
}
}
@@ -1551,56 +1500,35 @@
* Returns if bouncer expansion is between 0 and 1 non-inclusive.
*/
public boolean isPrimaryBouncerInTransit() {
- if (mPrimaryBouncer != null) {
- return mPrimaryBouncer.inTransit();
- } else {
- return mPrimaryBouncerInteractor.isInTransit();
- }
+ return mPrimaryBouncerInteractor.isInTransit();
}
/**
* Returns if bouncer is showing
*/
public boolean primaryBouncerIsShowing() {
- if (mPrimaryBouncer != null) {
- return mPrimaryBouncer.isShowing();
- } else {
- return mPrimaryBouncerInteractor.isFullyShowing();
- }
+ return mPrimaryBouncerInteractor.isFullyShowing();
}
/**
* Returns if bouncer is scrimmed
*/
public boolean primaryBouncerIsScrimmed() {
- if (mPrimaryBouncer != null) {
- return mPrimaryBouncer.isScrimmed();
- } else {
- return mPrimaryBouncerInteractor.isScrimmed();
- }
+ return mPrimaryBouncerInteractor.isScrimmed();
}
/**
* Returns if bouncer is animating away
*/
public boolean bouncerIsAnimatingAway() {
- if (mPrimaryBouncer != null) {
- return mPrimaryBouncer.isAnimatingAway();
- } else {
- return mPrimaryBouncerInteractor.isAnimatingAway();
- }
-
+ return mPrimaryBouncerInteractor.isAnimatingAway();
}
/**
* Returns if bouncer will dismiss with action
*/
public boolean primaryBouncerWillDismissWithAction() {
- if (mPrimaryBouncer != null) {
- return mPrimaryBouncer.willDismissWithAction();
- } else {
- return mPrimaryBouncerInteractor.willDismissWithAction();
- }
+ return mPrimaryBouncerInteractor.willDismissWithAction();
}
/**
@@ -1614,10 +1542,9 @@
}
/**
- * Delegate used to send show and hide events to an alternate authentication method instead of
- * the regular pin/pattern/password bouncer.
+ * @Deprecated Delegate used to send show and hide events to an alternate bouncer.
*/
- public interface AlternateBouncer {
+ public interface LegacyAlternateBouncer {
/**
* Show alternate authentication bouncer.
* @return whether alternate auth method was newly shown
@@ -1634,7 +1561,13 @@
* @return true if the alternate auth bouncer is showing
*/
boolean isShowingAlternateBouncer();
+ }
+ /**
+ * Delegate used to send show and hide events to an alternate authentication method instead of
+ * the regular pin/pattern/password bouncer.
+ */
+ public interface OccludingAppBiometricUI {
/**
* Use when an app occluding the keyguard would like to give the user ability to
* unlock the device using udfps.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index be6e0cc..078a00d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -53,6 +53,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.NotificationClickNotifier;
@@ -118,6 +119,7 @@
private final NotificationPanelViewController mNotificationPanel;
private final ActivityLaunchAnimator mActivityLaunchAnimator;
private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
+ private final UserTracker mUserTracker;
private final OnUserInteractionCallback mOnUserInteractionCallback;
private boolean mIsCollapsingToShowActivityOverLockscreen;
@@ -153,7 +155,8 @@
ActivityLaunchAnimator activityLaunchAnimator,
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
LaunchFullScreenIntentProvider launchFullScreenIntentProvider,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ UserTracker userTracker) {
mContext = context;
mMainThreadHandler = mainThreadHandler;
mUiBgExecutor = uiBgExecutor;
@@ -184,6 +187,7 @@
mNotificationPanel = panel;
mActivityLaunchAnimator = activityLaunchAnimator;
mNotificationAnimationProvider = notificationAnimationProvider;
+ mUserTracker = userTracker;
launchFullScreenIntentProvider.registerListener(entry -> launchFullScreenIntent(entry));
}
@@ -518,7 +522,7 @@
intent.getPackage(),
(adapter) -> tsb.startActivities(
getActivityOptions(mCentralSurfaces.getDisplayId(), adapter),
- UserHandle.CURRENT));
+ mUserTracker.getUserHandle()));
});
return true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
index ae48c2d3..50cce45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java
@@ -17,5 +17,5 @@
public interface StatusBarWindowCallback {
void onStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean bouncerShowing,
- boolean isDozing, boolean panelExpanded);
+ boolean isDozing, boolean panelExpanded, boolean isDreaming);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
index 6cd8c78..9e6bb20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
@@ -16,7 +16,9 @@
package com.android.systemui.statusbar.phone
+import android.view.InsetsFlags
import android.view.InsetsVisibilities
+import android.view.ViewDebug
import android.view.WindowInsetsController.Appearance
import android.view.WindowInsetsController.Behavior
import com.android.internal.statusbar.LetterboxDetails
@@ -148,4 +150,20 @@
) {
val letterboxesArray = letterboxes.toTypedArray()
val appearanceRegionsArray = appearanceRegions.toTypedArray()
+ override fun toString(): String {
+ val appearanceToString =
+ ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
+ return """SystemBarAttributesParams(
+ displayId=$displayId,
+ appearance=$appearanceToString,
+ appearanceRegions=$appearanceRegions,
+ navbarColorManagedByIme=$navbarColorManagedByIme,
+ behavior=$behavior,
+ requestedVisibilities=$requestedVisibilities,
+ packageName='$packageName',
+ letterboxes=$letterboxes,
+ letterboxesArray=${letterboxesArray.contentToString()},
+ appearanceRegionsArray=${appearanceRegionsArray.contentToString()}
+ )""".trimMargin()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 206c0aa..c1c6c88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -23,6 +23,7 @@
import android.view.View;
import android.view.ViewStub;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.LockIconView;
import com.android.systemui.R;
import com.android.systemui.battery.BatteryMeterView;
@@ -46,7 +47,6 @@
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationShelfController;
import com.android.systemui.statusbar.OperatorNameViewController;
-import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
@@ -68,6 +68,7 @@
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.settings.SecureSettings;
@@ -293,7 +294,6 @@
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
NotificationPanelViewController notificationPanelViewController,
- NetworkController networkController,
StatusBarStateController statusBarStateController,
CommandQueue commandQueue,
CarrierConfigTracker carrierConfigTracker,
@@ -301,7 +301,9 @@
OperatorNameViewController.Factory operatorNameViewControllerFactory,
SecureSettings secureSettings,
@Main Executor mainExecutor,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ StatusBarWindowStateController statusBarWindowStateController,
+ KeyguardUpdateMonitor keyguardUpdateMonitor
) {
return new CollapsedStatusBarFragment(statusBarFragmentComponentFactory,
ongoingCallController,
@@ -315,7 +317,6 @@
statusBarHideIconsForBouncerManager,
keyguardStateController,
notificationPanelViewController,
- networkController,
statusBarStateController,
commandQueue,
carrierConfigTracker,
@@ -323,7 +324,9 @@
operatorNameViewControllerFactory,
secureSettings,
mainExecutor,
- dumpManager);
+ dumpManager,
+ statusBarWindowStateController,
+ keyguardUpdateMonitor);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 9f3fd72..00fd4ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -24,7 +24,6 @@
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT;
import android.animation.Animator;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.Fragment;
@@ -45,6 +44,7 @@
import androidx.annotation.VisibleForTesting;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
@@ -59,9 +59,6 @@
import com.android.systemui.statusbar.OperatorNameView;
import com.android.systemui.statusbar.OperatorNameViewController;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.connectivity.IconState;
-import com.android.systemui.statusbar.connectivity.NetworkController;
-import com.android.systemui.statusbar.connectivity.SignalCallback;
import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -75,8 +72,9 @@
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
-import com.android.systemui.statusbar.policy.EncryptionHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.CarrierConfigTracker.CarrierConfigChangedListener;
import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
@@ -110,7 +108,6 @@
private final StatusBarStateController mStatusBarStateController;
private final KeyguardStateController mKeyguardStateController;
private final NotificationPanelViewController mNotificationPanelViewController;
- private final NetworkController mNetworkController;
private LinearLayout mEndSideContent;
private View mClockView;
private View mOngoingCallChip;
@@ -135,17 +132,12 @@
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final DumpManager mDumpManager;
+ private final StatusBarWindowStateController mStatusBarWindowStateController;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private List<String> mBlockedIcons = new ArrayList<>();
private Map<Startable, Startable.State> mStartableStates = new ArrayMap<>();
- private SignalCallback mSignalCallback = new SignalCallback() {
- @Override
- public void setIsAirplaneMode(@NonNull IconState icon) {
- mCommandQueue.recomputeDisableFlags(getContext().getDisplayId(), true /* animate */);
- }
- };
-
private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() {
@Override
public void onOngoingCallStateChanged(boolean animate) {
@@ -177,6 +169,22 @@
}
};
+ /**
+ * Whether we've launched the secure camera over the lockscreen, but haven't yet received a
+ * status bar window state change afterward.
+ *
+ * We wait for this state change (which will tell us whether to show/hide the status bar icons)
+ * so that there is no flickering/jump cutting during the camera launch.
+ */
+ private boolean mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
+ /**
+ * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives
+ * a new status bar window state.
+ */
+ private final StatusBarWindowStateListener mStatusBarWindowStateListener = state ->
+ mWaitingForWindowStateChangeAfterCameraLaunch = false;
+
@SuppressLint("ValidFragment")
public CollapsedStatusBarFragment(
StatusBarFragmentComponent.Factory statusBarFragmentComponentFactory,
@@ -191,7 +199,6 @@
StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
KeyguardStateController keyguardStateController,
NotificationPanelViewController notificationPanelViewController,
- NetworkController networkController,
StatusBarStateController statusBarStateController,
CommandQueue commandQueue,
CarrierConfigTracker carrierConfigTracker,
@@ -199,7 +206,9 @@
OperatorNameViewController.Factory operatorNameViewControllerFactory,
SecureSettings secureSettings,
@Main Executor mainExecutor,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ StatusBarWindowStateController statusBarWindowStateController,
+ KeyguardUpdateMonitor keyguardUpdateMonitor
) {
mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
mOngoingCallController = ongoingCallController;
@@ -213,7 +222,6 @@
mDarkIconManagerFactory = darkIconManagerFactory;
mKeyguardStateController = keyguardStateController;
mNotificationPanelViewController = notificationPanelViewController;
- mNetworkController = networkController;
mStatusBarStateController = statusBarStateController;
mCommandQueue = commandQueue;
mCarrierConfigTracker = carrierConfigTracker;
@@ -222,6 +230,20 @@
mSecureSettings = secureSettings;
mMainExecutor = mainExecutor;
mDumpManager = dumpManager;
+ mStatusBarWindowStateController = statusBarWindowStateController;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
}
@Override
@@ -261,7 +283,6 @@
mOngoingCallChip = mStatusBar.findViewById(R.id.ongoing_call_chip);
showEndSideContent(false);
showClock(false);
- initEmergencyCryptkeeperText();
initOperatorName();
initNotificationIconArea();
mSystemEventAnimator =
@@ -270,6 +291,11 @@
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
}
+ @Override
+ public void onCameraLaunchGestureDetected(int source) {
+ mWaitingForWindowStateChangeAfterCameraLaunch = true;
+ }
+
@VisibleForTesting
void updateBlockedIcons() {
mBlockedIcons.clear();
@@ -320,7 +346,7 @@
mAnimationScheduler.addCallback(this);
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON),
+ Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
false,
mVolumeSettingObserver,
UserHandle.USER_ALL);
@@ -340,9 +366,6 @@
public void onDestroyView() {
super.onDestroyView();
mStatusBarIconController.removeIconGroup(mDarkIconManager);
- if (mNetworkController.hasEmergencyCryptKeeperText()) {
- mNetworkController.removeCallback(mSignalCallback);
- }
mCarrierConfigTracker.removeCallback(mCarrierConfigCallback);
mCarrierConfigTracker.removeDataSubscriptionChangedListener(mDefaultDataListener);
@@ -444,15 +467,6 @@
state |= DISABLE_CLOCK;
}
- if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
- if (mNetworkController.hasEmergencyCryptKeeperText()) {
- state |= DISABLE_NOTIFICATION_ICONS;
- }
- if (!mNetworkController.isRadioOn()) {
- state |= DISABLE_SYSTEM_INFO;
- }
- }
-
if (mOngoingCallController.hasOngoingCall()) {
state &= ~DISABLE_ONGOING_CALL_CHIP;
} else {
@@ -494,6 +508,27 @@
&& mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) {
return true;
}
+
+ // When launching the camera over the lockscreen, the icons become visible momentarily
+ // before animating out, since we're not yet aware that the launching camera activity is
+ // fullscreen. Even once the activity finishes launching, it takes a short time before WM
+ // decides that the top app wants to hide the icons and tells us to hide them. To ensure
+ // that this high-visibility animation is smooth, keep the icons hidden during a camera
+ // launch until we receive a window state change which indicates that the activity is done
+ // launching and WM has decided to show/hide the icons. For extra safety (to ensure the
+ // icons don't remain hidden somehow) we double check that the camera is still showing, the
+ // status bar window isn't hidden, and we're still occluded as well, though these checks
+ // are typically unnecessary.
+ final boolean hideIconsForSecureCamera =
+ (mWaitingForWindowStateChangeAfterCameraLaunch ||
+ !mStatusBarWindowStateController.windowIsShowing()) &&
+ mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard() &&
+ mKeyguardStateController.isOccluded();
+
+ if (hideIconsForSecureCamera) {
+ return true;
+ }
+
return mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer();
}
@@ -620,19 +655,6 @@
}
}
- private void initEmergencyCryptkeeperText() {
- View emergencyViewStub = mStatusBar.findViewById(R.id.emergency_cryptkeeper_text);
- if (mNetworkController.hasEmergencyCryptKeeperText()) {
- if (emergencyViewStub != null) {
- ((ViewStub) emergencyViewStub).inflate();
- }
- mNetworkController.addCallback(mSignalCallback);
- } else if (emergencyViewStub != null) {
- ViewGroup parent = (ViewGroup) emergencyViewStub.getParent();
- parent.removeView(emergencyViewStub);
- }
- }
-
private void initOperatorName() {
int subId = SubscriptionManager.getDefaultDataSubscriptionId();
if (mCarrierConfigTracker.getShowOperatorNameInStatusBarConfig(subId)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
index 2c8677d..2d80edb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
@@ -19,14 +19,14 @@
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
-import android.widget.LinearLayout
import android.widget.TextView
import com.android.systemui.R
+import com.android.systemui.common.ui.view.LaunchableLinearLayout
class StatusBarUserSwitcherContainer(
context: Context?,
attrs: AttributeSet?
-) : LinearLayout(context, attrs) {
+) : LaunchableLinearLayout(context, attrs) {
lateinit var text: TextView
private set
lateinit var avatar: ImageView
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
index 5960387..5562e73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.data.model
import android.telephony.Annotation.NetworkType
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
/**
@@ -38,4 +40,12 @@
data class OverrideNetworkType(
override val lookupKey: String,
) : ResolvedNetworkType
+
+ /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */
+ object CarrierMergedNetworkType : ResolvedNetworkType {
+ // Effectively unused since [iconGroupOverride] is used instead.
+ override val lookupKey: String = "cwf"
+
+ val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index d04996b..6187f64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -22,7 +22,6 @@
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
@@ -50,7 +49,7 @@
* A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
* listener + model.
*/
- val connectionInfo: Flow<MobileConnectionModel>
+ val connectionInfo: StateFlow<MobileConnectionModel>
/** The total number of levels. Used with [SignalDrawable]. */
val numberOfLevels: StateFlow<Int>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index 97b4c2c..e0d156a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -18,6 +18,7 @@
import android.provider.Settings
import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.MobileMappings.Config
@@ -37,6 +38,15 @@
/** Observable for the subscriptionId of the current mobile data connection */
val activeMobileDataSubscriptionId: StateFlow<Int>
+ /**
+ * Observable event for when the active data sim switches but the group stays the same. E.g.,
+ * CBRS switching would trigger this
+ */
+ val activeSubChangedInGroupEvent: Flow<Unit>
+
+ /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
+ val defaultDataSubId: StateFlow<Int>
+
/** The current connectivity status for the default mobile network connection */
val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
index 0c8593d6..b939856 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -124,6 +124,9 @@
realRepository.activeMobileDataSubscriptionId.value
)
+ override val activeSubChangedInGroupEvent: Flow<Unit> =
+ activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent }
+
override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
activeRepo
.flatMapLatest { it.defaultDataSubRatConfig }
@@ -139,6 +142,11 @@
override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
activeRepo.flatMapLatest { it.defaultMobileIconGroup }
+ override val defaultDataSubId: StateFlow<Int> =
+ activeRepo
+ .flatMapLatest { it.defaultDataSubId }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
+
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
activeRepo
.flatMapLatest { it.defaultMobileNetworkConnectivity }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 0e164e7..1088345 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -39,11 +39,16 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -60,15 +65,19 @@
class DemoMobileConnectionsRepository
@Inject
constructor(
- private val dataSource: DemoModeMobileConnectionDataSource,
+ private val mobileDataSource: DemoModeMobileConnectionDataSource,
+ private val wifiDataSource: DemoModeWifiDataSource,
@Application private val scope: CoroutineScope,
context: Context,
private val logFactory: TableLogBufferFactory,
) : MobileConnectionsRepository {
- private var demoCommandJob: Job? = null
+ private var mobileDemoCommandJob: Job? = null
+ private var wifiDemoCommandJob: Job? = null
- private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>()
+ private var carrierMergedSubId: Int? = null
+
+ private var connectionRepoCache = mutableMapOf<Int, CacheContainer>()
private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>()
val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
@@ -112,6 +121,9 @@
subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
)
+ // TODO(b/261029387): consider adding a demo command for this
+ override val activeSubChangedInGroupEvent: Flow<Unit> = flowOf()
+
/** Demo mode doesn't currently support modifications to the mobile mappings */
override val defaultDataSubRatConfig =
MutableStateFlow(MobileMappings.Config.readConfig(context))
@@ -140,56 +152,94 @@
private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key }
+ // TODO(b/261029387): add a command for this value
+ override val defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+
// TODO(b/261029387): not yet supported
- override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
+ override val defaultMobileNetworkConnectivity =
+ MutableStateFlow(MobileConnectivityModel(isConnected = true, isValidated = true))
override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
- return connectionRepoCache[subId]
- ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it }
+ val current = connectionRepoCache[subId]?.repo
+ if (current != null) {
+ return current
+ }
+
+ val new = createDemoMobileConnectionRepo(subId)
+ connectionRepoCache[subId] = new
+ return new.repo
}
- private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository {
- val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100)
+ private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer {
+ val tableLogBuffer =
+ logFactory.getOrCreate(
+ "DemoMobileConnectionLog [$subId]",
+ MOBILE_CONNECTION_BUFFER_SIZE,
+ )
- return DemoMobileConnectionRepository(
- subId,
- tableLogBuffer,
- )
+ val repo =
+ DemoMobileConnectionRepository(
+ subId,
+ tableLogBuffer,
+ )
+ return CacheContainer(repo, lastMobileState = null)
}
override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
fun startProcessingCommands() {
- demoCommandJob =
+ mobileDemoCommandJob =
scope.launch {
- dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) }
+ mobileDataSource.mobileEvents.filterNotNull().collect { event ->
+ processMobileEvent(event)
+ }
+ }
+ wifiDemoCommandJob =
+ scope.launch {
+ wifiDataSource.wifiEvents.filterNotNull().collect { event ->
+ processWifiEvent(event)
+ }
}
}
fun stopProcessingCommands() {
- demoCommandJob?.cancel()
+ mobileDemoCommandJob?.cancel()
+ wifiDemoCommandJob?.cancel()
_subscriptions.value = listOf()
connectionRepoCache.clear()
subscriptionInfoCache.clear()
}
- private fun processEvent(event: FakeNetworkEventModel) {
+ private fun processMobileEvent(event: FakeNetworkEventModel) {
when (event) {
is Mobile -> {
processEnabledMobileState(event)
}
is MobileDisabled -> {
- processDisabledMobileState(event)
+ maybeRemoveSubscription(event.subId)
}
}
}
+ private fun processWifiEvent(event: FakeWifiEventModel) {
+ when (event) {
+ is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged()
+ is FakeWifiEventModel.Wifi -> disableCarrierMerged()
+ is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
+ }
+ }
+
private fun processEnabledMobileState(state: Mobile) {
// get or create the connection repo, and set its values
val subId = state.subId ?: DEFAULT_SUB_ID
maybeCreateSubscription(subId)
val connection = getRepoForSubId(subId)
+ connectionRepoCache[subId]?.lastMobileState = state
+
+ // TODO(b/261029387): until we have a command, use the most recent subId
+ defaultDataSubId.value = subId
+
// This is always true here, because we split out disabled states at the data-source level
connection.dataEnabled.value = true
connection.networkName.value = NetworkNameModel.Derived(state.name)
@@ -198,14 +248,36 @@
connection.connectionInfo.value = state.toMobileConnectionModel()
}
- private fun processDisabledMobileState(state: MobileDisabled) {
+ private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+ // The new carrier merged connection is for a different sub ID, so disable carrier merged
+ // for the current (now old) sub
+ if (carrierMergedSubId != event.subscriptionId) {
+ disableCarrierMerged()
+ }
+
+ // get or create the connection repo, and set its values
+ val subId = event.subscriptionId
+ maybeCreateSubscription(subId)
+ carrierMergedSubId = subId
+
+ val connection = getRepoForSubId(subId)
+ // This is always true here, because we split out disabled states at the data-source level
+ connection.dataEnabled.value = true
+ connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME)
+ connection.numberOfLevels.value = event.numberOfLevels
+ connection.cdmaRoaming.value = false
+ connection.connectionInfo.value = event.toMobileConnectionModel()
+ Log.e("CCS", "output connection info = ${connection.connectionInfo.value}")
+ }
+
+ private fun maybeRemoveSubscription(subId: Int?) {
if (_subscriptions.value.isEmpty()) {
// Nothing to do here
return
}
- val subId =
- state.subId
+ val finalSubId =
+ subId
?: run {
// For sake of usability, we can allow for no subId arg if there is only one
// subscription
@@ -223,7 +295,21 @@
_subscriptions.value[0].subscriptionId
}
- removeSubscription(subId)
+ removeSubscription(finalSubId)
+ }
+
+ private fun disableCarrierMerged() {
+ val currentCarrierMergedSubId = carrierMergedSubId ?: return
+
+ // If this sub ID was previously not carrier merged, we should reset it to its previous
+ // connection.
+ val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState
+ if (lastMobileState != null) {
+ processEnabledMobileState(lastMobileState)
+ } else {
+ // Otherwise, just remove the subscription entirely
+ removeSubscription(currentCarrierMergedSubId)
+ }
}
private fun removeSubscription(subId: Int) {
@@ -251,6 +337,10 @@
)
}
+ private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel {
+ return createCarrierMergedConnectionModel(this.level)
+ }
+
private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
val key = mobileMappingsReverseLookup.value[this] ?: "dis"
return DefaultNetworkType(key)
@@ -260,9 +350,17 @@
private const val TAG = "DemoMobileConnectionsRepo"
private const val DEFAULT_SUB_ID = 1
+
+ private const val CARRIER_MERGED_NAME = "Carrier Merged Network"
}
}
+class CacheContainer(
+ var repo: DemoMobileConnectionRepository,
+ /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */
+ var lastMobileState: Mobile?,
+)
+
class DemoMobileConnectionRepository(
override val subId: Int,
override val tableLogBuffer: TableLogBuffer,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
new file mode 100644
index 0000000..c783b12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is
+ * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually
+ * displayed as a mobile network triangle.
+ *
+ * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+ *
+ * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile
+ * connection.
+ */
+class CarrierMergedConnectionRepository(
+ override val subId: Int,
+ override val tableLogBuffer: TableLogBuffer,
+ defaultNetworkName: NetworkNameModel,
+ @Application private val scope: CoroutineScope,
+ val wifiRepository: WifiRepository,
+) : MobileConnectionRepository {
+
+ /**
+ * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged
+ * network.
+ */
+ private val network: Flow<WifiNetworkModel.CarrierMerged?> =
+ combine(
+ wifiRepository.isWifiEnabled,
+ wifiRepository.isWifiDefault,
+ wifiRepository.wifiNetwork,
+ ) { isEnabled, isDefault, network ->
+ when {
+ !isEnabled -> null
+ !isDefault -> null
+ network !is WifiNetworkModel.CarrierMerged -> null
+ network.subscriptionId != subId -> {
+ Log.w(
+ TAG,
+ "Connection repo subId=$subId " +
+ "does not equal wifi repo subId=${network.subscriptionId}; " +
+ "not showing carrier merged"
+ )
+ null
+ }
+ else -> network
+ }
+ }
+
+ override val connectionInfo: StateFlow<MobileConnectionModel> =
+ network
+ .map { it.toMobileConnectionModel() }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel())
+
+ // TODO(b/238425913): Add logging to this class.
+ // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate.
+
+ // Carrier merged is never roaming.
+ override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
+
+ // TODO(b/238425913): Fetch the carrier merged network name.
+ override val networkName: StateFlow<NetworkNameModel> =
+ flowOf(defaultNetworkName)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
+ override val numberOfLevels: StateFlow<Int> =
+ wifiRepository.wifiNetwork
+ .map {
+ if (it is WifiNetworkModel.CarrierMerged) {
+ it.numberOfLevels
+ } else {
+ DEFAULT_NUM_LEVELS
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+
+ override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled
+
+ private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel {
+ if (this == null) {
+ return MobileConnectionModel()
+ }
+
+ return createCarrierMergedConnectionModel(level)
+ }
+
+ companion object {
+ /**
+ * Creates an instance of [MobileConnectionModel] that represents a carrier merged network
+ * with the given [level].
+ */
+ fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel {
+ return MobileConnectionModel(
+ primaryLevel = level,
+ cdmaLevel = level,
+ // A [WifiNetworkModel.CarrierMerged] instance is always connected.
+ // (A [WifiNetworkModel.Inactive] represents a disconnected network.)
+ dataConnectionState = DataConnectionState.Connected,
+ // TODO(b/238425913): This should come from [WifiRepository.wifiActivity].
+ dataActivityDirection =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ ),
+ resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+ // Carrier merged is never roaming
+ isRoaming = false,
+
+ // TODO(b/238425913): Verify that these fields never change for carrier merged.
+ isEmergencyOnly = false,
+ operatorAlphaShort = null,
+ isInService = true,
+ isGsm = false,
+ carrierNetworkChangeActive = false,
+ )
+ }
+ }
+
+ @SysUISingleton
+ class Factory
+ @Inject
+ constructor(
+ @Application private val scope: CoroutineScope,
+ private val wifiRepository: WifiRepository,
+ ) {
+ fun build(
+ subId: Int,
+ mobileLogger: TableLogBuffer,
+ defaultNetworkName: NetworkNameModel,
+ ): MobileConnectionRepository {
+ return CarrierMergedConnectionRepository(
+ subId,
+ mobileLogger,
+ defaultNetworkName,
+ scope,
+ wifiRepository,
+ )
+ }
+ }
+}
+
+private const val TAG = "CarrierMergedConnectionRepository"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
new file mode 100644
index 0000000..0f30ae2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+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.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A repository that fully implements a mobile connection.
+ *
+ * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl]
+ * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository
+ * switches between the two types of connections based on whether the connection is currently
+ * carrier merged (see [setIsCarrierMerged]).
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class FullMobileConnectionRepository(
+ override val subId: Int,
+ startingIsCarrierMerged: Boolean,
+ override val tableLogBuffer: TableLogBuffer,
+ private val defaultNetworkName: NetworkNameModel,
+ private val networkNameSeparator: String,
+ private val globalMobileDataSettingChangedEvent: Flow<Unit>,
+ @Application scope: CoroutineScope,
+ private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+ private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+) : MobileConnectionRepository {
+ /**
+ * Sets whether this connection is a typical mobile connection or a carrier merged connection.
+ */
+ fun setIsCarrierMerged(isCarrierMerged: Boolean) {
+ _isCarrierMerged.value = isCarrierMerged
+ }
+
+ /**
+ * Returns true if this repo is currently for a carrier merged connection and false otherwise.
+ */
+ @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value
+
+ private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged)
+ private val isCarrierMerged: StateFlow<Boolean> =
+ _isCarrierMerged
+ .logDiffsForTable(
+ tableLogBuffer,
+ columnPrefix = "",
+ columnName = "isCarrierMerged",
+ initialValue = startingIsCarrierMerged,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged)
+
+ private val mobileRepo: MobileConnectionRepository by lazy {
+ mobileRepoFactory.build(
+ subId,
+ tableLogBuffer,
+ defaultNetworkName,
+ networkNameSeparator,
+ globalMobileDataSettingChangedEvent,
+ )
+ }
+
+ private val carrierMergedRepo: MobileConnectionRepository by lazy {
+ carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName)
+ }
+
+ @VisibleForTesting
+ internal val activeRepo: StateFlow<MobileConnectionRepository> = run {
+ val initial =
+ if (startingIsCarrierMerged) {
+ carrierMergedRepo
+ } else {
+ mobileRepo
+ }
+
+ this.isCarrierMerged
+ .mapLatest { isCarrierMerged ->
+ if (isCarrierMerged) {
+ carrierMergedRepo
+ } else {
+ mobileRepo
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
+
+ override val cdmaRoaming =
+ activeRepo
+ .flatMapLatest { it.cdmaRoaming }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value)
+
+ override val connectionInfo =
+ activeRepo
+ .flatMapLatest { it.connectionInfo }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value)
+
+ override val dataEnabled =
+ activeRepo
+ .flatMapLatest { it.dataEnabled }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
+
+ override val numberOfLevels =
+ activeRepo
+ .flatMapLatest { it.numberOfLevels }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value)
+
+ override val networkName =
+ activeRepo
+ .flatMapLatest { it.networkName }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
+
+ class Factory
+ @Inject
+ constructor(
+ @Application private val scope: CoroutineScope,
+ private val logFactory: TableLogBufferFactory,
+ private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
+ private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
+ ) {
+ fun build(
+ subId: Int,
+ startingIsCarrierMerged: Boolean,
+ defaultNetworkName: NetworkNameModel,
+ networkNameSeparator: String,
+ globalMobileDataSettingChangedEvent: Flow<Unit>,
+ ): FullMobileConnectionRepository {
+ val mobileLogger =
+ logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
+
+ return FullMobileConnectionRepository(
+ subId,
+ startingIsCarrierMerged,
+ mobileLogger,
+ defaultNetworkName,
+ networkNameSeparator,
+ globalMobileDataSettingChangedEvent,
+ scope,
+ mobileRepoFactory,
+ carrierMergedRepoFactory,
+ )
+ }
+
+ companion object {
+ /** The buffer size to use for logging. */
+ const val MOBILE_CONNECTION_BUFFER_SIZE = 100
+
+ /** Returns a log buffer name for a mobile connection with the given [subId]. */
+ fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 0fa0fea..3f2ce40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -38,7 +38,6 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
@@ -70,6 +69,10 @@
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+/**
+ * A repository implementation for a typical mobile connection (as opposed to a carrier merged
+ * connection -- see [CarrierMergedConnectionRepository]).
+ */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileConnectionRepositoryImpl(
@@ -298,18 +301,16 @@
private val logger: ConnectivityPipelineLogger,
private val globalSettings: GlobalSettings,
private val mobileMappingsProxy: MobileMappingsProxy,
- private val logFactory: TableLogBufferFactory,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
fun build(
subId: Int,
+ mobileLogger: TableLogBuffer,
defaultNetworkName: NetworkNameModel,
networkNameSeparator: String,
globalMobileDataSettingChangedEvent: Flow<Unit>,
): MobileConnectionRepository {
- val mobileLogger = logFactory.create(tableBufferLogName(subId), 100)
-
return MobileConnectionRepositoryImpl(
context,
subId,
@@ -327,8 +328,4 @@
)
}
}
-
- companion object {
- fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index c88c700..39ad31f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -35,6 +35,7 @@
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
+import com.android.internal.telephony.PhoneConstants
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings.Config
import com.android.systemui.R
@@ -46,11 +47,13 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -59,11 +62,14 @@
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
@@ -85,9 +91,14 @@
private val context: Context,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
- private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+ // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi
+ // repository is an input to the mobile repository.
+ // See [CarrierMergedConnectionRepository] for details.
+ wifiRepository: WifiRepository,
+ private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory,
) : MobileConnectionsRepository {
- private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+ private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> =
+ mutableMapOf()
private val defaultNetworkName =
NetworkNameModel.Default(
@@ -97,30 +108,43 @@
private val networkNameSeparator: String =
context.getString(R.string.status_bar_network_name_separator)
+ private val carrierMergedSubId: StateFlow<Int?> =
+ wifiRepository.wifiNetwork
+ .mapLatest {
+ if (it is WifiNetworkModel.CarrierMerged) {
+ it.subscriptionId
+ } else {
+ null
+ }
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
+
+ private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
+ val callback =
+ object : SubscriptionManager.OnSubscriptionsChangedListener() {
+ override fun onSubscriptionsChanged() {
+ trySend(Unit)
+ }
+ }
+
+ subscriptionManager.addOnSubscriptionsChangedListener(
+ bgDispatcher.asExecutor(),
+ callback,
+ )
+
+ awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+ }
+
/**
* State flow that emits the set of mobile data subscriptions, each represented by its own
- * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
- * info object, but for now we keep track of the infos themselves.
+ * [SubscriptionModel].
*/
override val subscriptions: StateFlow<List<SubscriptionModel>> =
- conflatedCallbackFlow {
- val callback =
- object : SubscriptionManager.OnSubscriptionsChangedListener() {
- override fun onSubscriptionsChanged() {
- trySend(Unit)
- }
- }
-
- subscriptionManager.addOnSubscriptionsChangedListener(
- bgDispatcher.asExecutor(),
- callback,
- )
-
- awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
- }
+ merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
.mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
.logInputChange(logger, "onSubscriptionsChanged")
- .onEach { infos -> dropUnusedReposFromCache(infos) }
+ .onEach { infos -> updateRepos(infos) }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
/** StateFlow that keeps track of the current active mobile data subscription */
@@ -140,10 +164,24 @@
.logInputChange(logger, "onActiveDataSubscriptionIdChanged")
.stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
- private val defaultDataSubIdChangedEvent =
+ private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
+ MutableSharedFlow(extraBufferCapacity = 1)
+
+ override val defaultDataSubId: StateFlow<Int> =
broadcastDispatcher
- .broadcastFlow(IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED))
+ .broadcastFlow(
+ IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ ) { intent, _ ->
+ intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+ }
+ .distinctUntilChanged()
.logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
+ .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ SubscriptionManager.getDefaultDataSubscriptionId()
+ )
private val carrierConfigChangedEvent =
broadcastDispatcher
@@ -151,7 +189,7 @@
.logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED")
override val defaultDataSubRatConfig: StateFlow<Config> =
- merge(defaultDataSubIdChangedEvent, carrierConfigChangedEvent)
+ merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
.mapLatest { Config.readConfig(context) }
.distinctUntilChanged()
.logInputChange(logger, "defaultDataSubRatConfig")
@@ -173,7 +211,7 @@
.distinctUntilChanged()
.logInputChange(logger, "defaultMobileIconGroup")
- override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+ override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository {
if (!isValidSubId(subId)) {
throw IllegalArgumentException(
"subscriptionId $subId is not in the list of valid subscriptions"
@@ -239,6 +277,26 @@
.logInputChange(logger, "defaultMobileNetworkConnectivity")
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
+ /**
+ * Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data
+ * subscription Id changes but the subscription group remains the same. In these cases, we want
+ * to retain the previous subscription's validation status for up to 2s to avoid flickering the
+ * icon.
+ *
+ * TODO(b/265164432): we should probably expose all change events, not just same group
+ */
+ @SuppressLint("MissingPermission")
+ override val activeSubChangedInGroupEvent =
+ activeMobileDataSubscriptionId
+ .pairwise()
+ .mapNotNull { (prevVal: Int, newVal: Int) ->
+ val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal)?.groupUuid
+ val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal)?.groupUuid
+
+ if (prevSub != null && prevSub == nextSub) Unit else null
+ }
+ .flowOn(bgDispatcher)
+
private fun isValidSubId(subId: Int): Boolean {
subscriptions.value.forEach {
if (it.subscriptionId == subId) {
@@ -251,15 +309,27 @@
@VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
- private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
- return mobileConnectionRepositoryFactory.build(
+ private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
+ return fullMobileRepoFactory.build(
subId,
+ isCarrierMerged(subId),
defaultNetworkName,
networkNameSeparator,
globalMobileDataSettingChangedEvent,
)
}
+ private fun updateRepos(newInfos: List<SubscriptionModel>) {
+ dropUnusedReposFromCache(newInfos)
+ subIdRepositoryCache.forEach { (subId, repo) ->
+ repo.setIsCarrierMerged(isCarrierMerged(subId))
+ }
+ }
+
+ private fun isCarrierMerged(subId: Int): Boolean {
+ return subId == carrierMergedSubId.value
+ }
+
private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
// Remove any connection repository from the cache that isn't in the new set of IDs. They
// will get garbage collected once their subscribers go away
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 9427c6b..9cdff96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -18,12 +18,14 @@
import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons.NOT_DEFAULT_DATA
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -41,12 +43,29 @@
/** The current mobile data activity */
val activity: Flow<DataActivityModel>
+ /**
+ * This bit is meant to be `true` if and only if the default network capabilities (see
+ * [android.net.ConnectivityManager.registerDefaultNetworkCallback]) result in a network that
+ * has the [android.net.NetworkCapabilities.TRANSPORT_CELLULAR] represented.
+ *
+ * Note that this differs from [isDataConnected], which is tracked by telephony and has to do
+ * with the state of using this mobile connection for data as opposed to just voice. It is
+ * possible for a mobile subscription to be connected but not be in a connected data state, and
+ * thus we wouldn't want to show the network type icon.
+ */
+ val isConnected: Flow<Boolean>
+
+ /**
+ * True when telephony tells us that the data state is CONNECTED. See
+ * [android.telephony.TelephonyCallback.DataConnectionStateListener] for more details. We
+ * consider this connection to be serving data, and thus want to show a network type icon, when
+ * data is connected. Other data connection states would typically cause us not to show the icon
+ */
+ val isDataConnected: StateFlow<Boolean>
+
/** Only true if mobile is the default transport but is not validated, otherwise false */
val isDefaultConnectionFailed: StateFlow<Boolean>
- /** True when telephony tells us that the data state is CONNECTED */
- val isDataConnected: StateFlow<Boolean>
-
/** True if we consider this connection to be in service, i.e. can make calls */
val isInService: StateFlow<Boolean>
@@ -100,8 +119,10 @@
defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
override val alwaysShowDataRatIcon: StateFlow<Boolean>,
override val alwaysUseCdmaLevel: StateFlow<Boolean>,
+ defaultMobileConnectivity: StateFlow<MobileConnectivityModel>,
defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
defaultMobileIconGroup: StateFlow<MobileIconGroup>,
+ defaultDataSubId: StateFlow<Int>,
override val isDefaultConnectionFailed: StateFlow<Boolean>,
connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
@@ -111,8 +132,19 @@
override val activity = connectionInfo.mapLatest { it.dataActivityDirection }
+ override val isConnected: Flow<Boolean> = defaultMobileConnectivity.mapLatest { it.isConnected }
+
override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
+ private val isDefault =
+ defaultDataSubId
+ .mapLatest { connectionRepository.subId == it }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ connectionRepository.subId == defaultDataSubId.value
+ )
+
override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
override val networkName =
@@ -137,8 +169,17 @@
connectionInfo,
defaultMobileIconMapping,
defaultMobileIconGroup,
- ) { info, mapping, defaultGroup ->
- mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+ isDefault,
+ ) { info, mapping, defaultGroup, isDefault ->
+ if (!isDefault) {
+ return@combine NOT_DEFAULT_DATA
+ }
+
+ when (info.resolvedNetworkType) {
+ is ResolvedNetworkType.CarrierMergedNetworkType ->
+ info.resolvedNetworkType.iconGroupOverride
+ else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
+ }
}
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 83da1dd..0e4a432 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -23,22 +23,29 @@
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
/**
* Business layer logic for the set of mobile subscription icons.
@@ -62,6 +69,17 @@
/** True if the CDMA level should be preferred over the primary level. */
val alwaysUseCdmaLevel: StateFlow<Boolean>
+ /** Tracks the subscriptionId set as the default for data connections */
+ val defaultDataSubId: StateFlow<Int>
+
+ /**
+ * The connectivity of the default mobile network. Note that this can differ from what is
+ * reported from [MobileConnectionsRepository] in some cases. E.g., when the active subscription
+ * changes but the groupUuid remains the same, we keep the old validation information for 2
+ * seconds to avoid icon flickering.
+ */
+ val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
+
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
@@ -85,6 +103,7 @@
constructor(
private val mobileConnectionsRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker,
+ private val logger: ConnectivityPipelineLogger,
userSetupRepo: UserSetupRepository,
@Application private val scope: CoroutineScope,
) : MobileIconsInteractor {
@@ -153,6 +172,50 @@
}
}
}
+ .distinctUntilChanged()
+ .onEach { logger.logFilteredSubscriptionsChanged(it) }
+
+ override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
+
+ /**
+ * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
+ * validated bit from the old active network (A) while data is changing to the new one (B).
+ *
+ * This condition only applies if
+ * 1. A and B are in the same subscription group (e.c. for CBRS data switching) and
+ * 2. A was validated before the switch
+ *
+ * The goal of this is to minimize the flickering in the UI of the cellular indicator
+ */
+ private val forcingCellularValidation =
+ mobileConnectionsRepo.activeSubChangedInGroupEvent
+ .filter { mobileConnectionsRepo.defaultMobileNetworkConnectivity.value.isValidated }
+ .transformLatest {
+ emit(true)
+ delay(2000)
+ emit(false)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+ combine(
+ mobileConnectionsRepo.defaultMobileNetworkConnectivity,
+ forcingCellularValidation,
+ ) { networkConnectivity, forceValidation ->
+ return@combine if (forceValidation) {
+ MobileConnectivityModel(
+ isValidated = true,
+ isConnected = networkConnectivity.isConnected
+ )
+ } else {
+ networkConnectivity
+ }
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ mobileConnectionsRepo.defaultMobileNetworkConnectivity.value
+ )
/**
* Mapping from network type to [MobileIconGroup] using the config generated for the default
@@ -207,8 +270,10 @@
activeDataConnectionHasDataEnabled,
alwaysShowDataRatIcon,
alwaysUseCdmaLevel,
+ defaultMobileNetworkConnectivity,
defaultMobileIconMapping,
defaultMobileIconGroup,
+ defaultDataSubId,
isDefaultConnectionFailed,
mobileConnectionsRepo.getRepoForSubId(subId),
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 829a5ca..ef75713 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -23,6 +23,8 @@
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,7 +32,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -51,13 +55,17 @@
interactor: MobileIconsInteractor,
private val iconController: StatusBarIconController,
private val iconsViewModelFactory: MobileIconsViewModel.Factory,
+ private val logger: ConnectivityPipelineLogger,
@Application private val scope: CoroutineScope,
private val statusBarPipelineFlags: StatusBarPipelineFlags,
) : CoreStartable {
private val mobileSubIds: Flow<List<Int>> =
- interactor.filteredSubscriptions.mapLatest { subscriptions ->
- subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
- }
+ interactor.filteredSubscriptions
+ .mapLatest { subscriptions ->
+ subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
+ }
+ .distinctUntilChanged()
+ .onEach { logger.logUiAdapterSubIdsUpdated(it) }
/**
* We expose the list of tracked subscriptions as a flow of a list of ints, where each int is
@@ -72,6 +80,9 @@
/** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */
val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState)
+ private var isCollecting: Boolean = false
+ private var lastValue: List<Int>? = null
+
override fun start() {
// Only notify the icon controller if we want to *render* the new icons.
// Note that this flow may still run if
@@ -79,8 +90,18 @@
// get the logging data without rendering.
if (statusBarPipelineFlags.useNewMobileIcons()) {
scope.launch {
- mobileSubIds.collectLatest { iconController.setNewMobileIconSubIds(it) }
+ isCollecting = true
+ mobileSubIds.collectLatest {
+ logger.logUiAdapterSubIdsSentToIconController(it)
+ lastValue = it
+ iconController.setNewMobileIconSubIds(it)
+ }
}
}
}
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("isCollecting=$isCollecting")
+ pw.println("Last values sent to icon controller: $lastValue")
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index a2117c7..5e935616 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -102,24 +102,29 @@
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
+ private val showNetworkTypeIcon: Flow<Boolean> =
+ combine(
+ iconInteractor.isDataConnected,
+ iconInteractor.isDataEnabled,
+ iconInteractor.isDefaultConnectionFailed,
+ iconInteractor.alwaysShowDataRatIcon,
+ iconInteractor.isConnected,
+ ) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected ->
+ alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected)
+ }
+
override val networkTypeIcon: Flow<Icon?> =
combine(
iconInteractor.networkTypeIconGroup,
- iconInteractor.isDataConnected,
- iconInteractor.isDataEnabled,
- iconInteractor.isDefaultConnectionFailed,
- iconInteractor.alwaysShowDataRatIcon,
- ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
+ showNetworkTypeIcon,
+ ) { networkTypeIconGroup, shouldShow ->
val desc =
if (networkTypeIconGroup.dataContentDescription != 0)
ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
else null
val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
return@combine when {
- alwaysShow -> icon
- !dataConnected -> null
- !dataEnabled -> null
- failedConnection -> null
+ !shouldShow -> null
else -> icon
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index d3ff357..7c7ffaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -25,6 +25,7 @@
import com.android.systemui.log.dagger.StatusBarConnectivityLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -97,15 +98,20 @@
)
}
- fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ fun logOnCapabilitiesChanged(
+ network: Network,
+ networkCapabilities: NetworkCapabilities,
+ isDefaultNetworkCallback: Boolean,
+ ) {
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
{
+ bool1 = isDefaultNetworkCallback
int1 = network.getNetId()
str1 = networkCapabilities.toString()
},
- { "onCapabilitiesChanged: net=$int1 capabilities=$str1" }
+ { "onCapabilitiesChanged[default=$bool1]: net=$int1 capabilities=$str1" }
)
}
@@ -196,6 +202,35 @@
)
}
+ // TODO(b/238425913): We should split this class into mobile-specific and wifi-specific loggers.
+
+ fun logFilteredSubscriptionsChanged(subs: List<SubscriptionModel>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Filtered subscriptions updated: $str1" },
+ )
+ }
+
+ fun logUiAdapterSubIdsUpdated(subs: List<Int>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Sub IDs in MobileUiAdapter updated internally: $str1" },
+ )
+ }
+
+ fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ { str1 = subs.toString() },
+ { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" },
+ )
+ }
+
companion object {
const val SB_LOGGING_TAG = "SbConnectivity"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
index cc0ec54..b1e2812 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -77,6 +77,17 @@
return binding.getShouldIconBeVisible()
}
+ /** See [StatusBarIconView.getDrawingRect]. */
+ override fun getDrawingRect(outRect: Rect) {
+ super.getDrawingRect(outRect)
+ val translationX = translationX.toInt()
+ val translationY = translationY.toInt()
+ outRect.left += translationX
+ outRect.right += translationX
+ outRect.top += translationY
+ outRect.bottom += translationY
+ }
+
/**
* Initializes this view.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index 4251d18..da2daf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -16,13 +16,18 @@
package com.android.systemui.statusbar.pipeline.wifi.data.model
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.annotation.VisibleForTesting
import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.log.table.Diffable
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
/** Provides information about the current wifi network. */
sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
+ // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of
+ // copy-pasting the column names for each sub-object.
+
/**
* A model representing that we couldn't fetch any wifi information.
*
@@ -41,8 +46,43 @@
override fun logFull(row: TableRowLogger) {
row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE)
row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
row.logChange(COL_VALIDATED, false)
row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+ row.logChange(COL_SSID, null)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ row.logChange(COL_PASSPOINT_NAME, null)
+ }
+ }
+
+ /**
+ * A model representing that the wifi information we received was invalid in some way.
+ */
+ data class Invalid(
+ /** A description of why the wifi information was invalid. */
+ val invalidReason: String,
+ ) : WifiNetworkModel() {
+ override fun toString() = "WifiNetwork.Invalid[$invalidReason]"
+ override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+ if (prevVal !is Invalid) {
+ logFull(row)
+ return
+ }
+
+ if (invalidReason != prevVal.invalidReason) {
+ row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+ row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+ row.logChange(COL_VALIDATED, false)
+ row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
row.logChange(COL_SSID, null)
row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
row.logChange(COL_ONLINE_SIGN_UP, false)
@@ -59,18 +99,21 @@
return
}
- if (prevVal is CarrierMerged) {
- // The only difference between CarrierMerged and Inactive is the type
- row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
- return
- }
-
- // When changing from Active to Inactive, we need to log diffs to all the fields.
- logFullNonActiveNetwork(TYPE_INACTIVE, row)
+ // When changing to Inactive, we need to log diffs to all the fields.
+ logFull(row)
}
override fun logFull(row: TableRowLogger) {
- logFullNonActiveNetwork(TYPE_INACTIVE, row)
+ row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+ row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
+ row.logChange(COL_VALIDATED, false)
+ row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT)
+ row.logChange(COL_SSID, null)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ row.logChange(COL_PASSPOINT_NAME, null)
}
}
@@ -80,22 +123,75 @@
*
* See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
*/
- object CarrierMerged : WifiNetworkModel() {
- override fun toString() = "WifiNetwork.CarrierMerged"
+ data class CarrierMerged(
+ /**
+ * The [android.net.Network.netId] we received from
+ * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
+ *
+ * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId].
+ */
+ val networkId: Int,
+
+ /**
+ * The subscription ID that this connection represents.
+ *
+ * Comes from [android.net.wifi.WifiInfo.getSubscriptionId].
+ *
+ * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid,
+ * then this is *not* a carrier merged network).
+ */
+ val subscriptionId: Int,
+
+ /**
+ * The signal level, guaranteed to be 0 <= level <= numberOfLevels.
+ */
+ val level: Int,
+
+ /**
+ * The maximum possible level.
+ */
+ val numberOfLevels: Int = DEFAULT_NUM_LEVELS,
+ ) : WifiNetworkModel() {
+ init {
+ require(level in MIN_VALID_LEVEL..numberOfLevels) {
+ "0 <= wifi level <= $numberOfLevels required; level was $level"
+ }
+ require(subscriptionId != INVALID_SUBSCRIPTION_ID) {
+ "subscription ID cannot be invalid"
+ }
+ }
override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
- if (prevVal is CarrierMerged) {
+ if (prevVal !is CarrierMerged) {
+ logFull(row)
return
}
- if (prevVal is Inactive) {
- // The only difference between CarrierMerged and Inactive is the type.
- row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
- return
+ if (prevVal.networkId != networkId) {
+ row.logChange(COL_NETWORK_ID, networkId)
}
+ if (prevVal.subscriptionId != subscriptionId) {
+ row.logChange(COL_SUB_ID, subscriptionId)
+ }
+ if (prevVal.level != level) {
+ row.logChange(COL_LEVEL, level)
+ }
+ if (prevVal.numberOfLevels != numberOfLevels) {
+ row.logChange(COL_NUM_LEVELS, numberOfLevels)
+ }
+ }
- // When changing from Active to CarrierMerged, we need to log diffs to all the fields.
- logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row)
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
+ row.logChange(COL_NETWORK_ID, networkId)
+ row.logChange(COL_SUB_ID, subscriptionId)
+ row.logChange(COL_VALIDATED, true)
+ row.logChange(COL_LEVEL, level)
+ row.logChange(COL_NUM_LEVELS, numberOfLevels)
+ row.logChange(COL_SSID, null)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ row.logChange(COL_PASSPOINT_NAME, null)
}
}
@@ -137,38 +233,50 @@
override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
if (prevVal !is Active) {
- row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+ logFull(row)
+ return
}
- if (prevVal !is Active || prevVal.networkId != networkId) {
+ if (prevVal.networkId != networkId) {
row.logChange(COL_NETWORK_ID, networkId)
}
- if (prevVal !is Active || prevVal.isValidated != isValidated) {
+ if (prevVal.isValidated != isValidated) {
row.logChange(COL_VALIDATED, isValidated)
}
- if (prevVal !is Active || prevVal.level != level) {
+ if (prevVal.level != level) {
row.logChange(COL_LEVEL, level)
}
- if (prevVal !is Active || prevVal.ssid != ssid) {
+ if (prevVal.ssid != ssid) {
row.logChange(COL_SSID, ssid)
}
// TODO(b/238425913): The passpoint-related values are frequently never used, so it
// would be great to not log them when they're not used.
- if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
+ if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
}
- if (prevVal !is Active ||
- prevVal.isOnlineSignUpForPasspointAccessPoint !=
+ if (prevVal.isOnlineSignUpForPasspointAccessPoint !=
isOnlineSignUpForPasspointAccessPoint) {
row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
}
- if (prevVal !is Active ||
- prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
+ if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
}
}
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+ row.logChange(COL_NETWORK_ID, networkId)
+ row.logChange(COL_SUB_ID, null)
+ row.logChange(COL_VALIDATED, isValidated)
+ row.logChange(COL_LEVEL, level)
+ row.logChange(COL_NUM_LEVELS, null)
+ row.logChange(COL_SSID, ssid)
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+ row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+ row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+ }
+
override fun toString(): String {
// Only include the passpoint-related values in the string if we have them. (Most
// networks won't have them so they'll be mostly clutter.)
@@ -189,21 +297,13 @@
companion object {
@VisibleForTesting
- internal const val MIN_VALID_LEVEL = 0
- @VisibleForTesting
internal const val MAX_VALID_LEVEL = 4
}
}
- internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) {
- row.logChange(COL_NETWORK_TYPE, type)
- row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
- row.logChange(COL_VALIDATED, false)
- row.logChange(COL_LEVEL, LEVEL_DEFAULT)
- row.logChange(COL_SSID, null)
- row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
- row.logChange(COL_ONLINE_SIGN_UP, false)
- row.logChange(COL_PASSPOINT_NAME, null)
+ companion object {
+ @VisibleForTesting
+ internal const val MIN_VALID_LEVEL = 0
}
}
@@ -214,12 +314,16 @@
const val COL_NETWORK_TYPE = "type"
const val COL_NETWORK_ID = "networkId"
+const val COL_SUB_ID = "subscriptionId"
const val COL_VALIDATED = "isValidated"
const val COL_LEVEL = "level"
+const val COL_NUM_LEVELS = "maxLevel"
const val COL_SSID = "ssid"
const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
val LEVEL_DEFAULT: String? = null
+val NUM_LEVELS_DEFAULT: String? = null
val NETWORK_ID_DEFAULT: String? = null
+val SUB_ID_DEFAULT: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
index c588945..caac8fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
@@ -22,6 +22,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -43,10 +44,10 @@
private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
val wifi = getString("wifi") ?: return null
- return if (wifi == "show") {
- activeWifiEvent()
- } else {
- FakeWifiEventModel.WifiDisabled
+ return when (wifi) {
+ "show" -> activeWifiEvent()
+ "carriermerged" -> carrierMergedWifiEvent()
+ else -> FakeWifiEventModel.WifiDisabled
}
}
@@ -64,6 +65,14 @@
)
}
+ private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged {
+ val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID
+ val level = getString("level")?.toInt() ?: 0
+ val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS
+
+ return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels)
+ }
+
private fun String.toActivity(): Int =
when (this) {
"inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
@@ -71,4 +80,8 @@
"out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
}
+
+ companion object {
+ const val DEFAULT_CARRIER_MERGED_SUB_ID = 10
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
index be3d7d4..e161b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -66,6 +66,7 @@
private fun processEvent(event: FakeWifiEventModel) =
when (event) {
is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
+ is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
}
@@ -85,6 +86,14 @@
_wifiNetwork.value = event.toWifiNetworkModel()
}
+ private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
+ _isWifiEnabled.value = true
+ _isWifiDefault.value = true
+ // TODO(b/238425913): Support activity in demo mode.
+ _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ _wifiNetwork.value = event.toCarrierMergedModel()
+ }
+
private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
WifiNetworkModel.Active(
networkId = DEMO_NET_ID,
@@ -99,6 +108,14 @@
passpointProviderFriendlyName = null,
)
+ private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel =
+ WifiNetworkModel.CarrierMerged(
+ networkId = DEMO_NET_ID,
+ subscriptionId = subscriptionId,
+ level = level,
+ numberOfLevels = numberOfLevels,
+ )
+
companion object {
private const val DEMO_NET_ID = 1234
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
index 2353fb8..518f8ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
@@ -29,5 +29,11 @@
val validated: Boolean?,
) : FakeWifiEventModel
+ data class CarrierMerged(
+ val subscriptionId: Int,
+ val level: Int,
+ val numberOfLevels: Int,
+ ) : FakeWifiEventModel
+
object WifiDisabled : FakeWifiEventModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index c47c20d..8669047 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -29,6 +29,7 @@
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import com.android.settingslib.Utils
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -113,13 +114,17 @@
network: Network,
networkCapabilities: NetworkCapabilities
) {
+ logger.logOnCapabilitiesChanged(
+ network,
+ networkCapabilities,
+ isDefaultNetworkCallback = true,
+ )
+
// This method will always be called immediately after the network
// becomes the default, in addition to any time the capabilities change
// while the network is the default.
- // If this network contains valid wifi info, then wifi is the default
- // network.
- val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
- trySend(wifiInfo != null)
+ // If this network is a wifi network, then wifi is the default network.
+ trySend(isWifiNetwork(networkCapabilities))
}
override fun onLost(network: Network) {
@@ -151,7 +156,11 @@
network: Network,
networkCapabilities: NetworkCapabilities
) {
- logger.logOnCapabilitiesChanged(network, networkCapabilities)
+ logger.logOnCapabilitiesChanged(
+ network,
+ networkCapabilities,
+ isDefaultNetworkCallback = false,
+ )
wifiNetworkChangeEvents.tryEmit(Unit)
@@ -252,16 +261,30 @@
networkCapabilities: NetworkCapabilities
): WifiInfo? {
return when {
- networkCapabilities.hasTransport(TRANSPORT_WIFI) ->
- networkCapabilities.transportInfo as WifiInfo?
networkCapabilities.hasTransport(TRANSPORT_CELLULAR) ->
// Sometimes, cellular networks can act as wifi networks (known as VCN --
// virtual carrier network). So, see if this cellular network has wifi info.
Utils.tryGetWifiInfoForVcn(networkCapabilities)
+ networkCapabilities.hasTransport(TRANSPORT_WIFI) ->
+ if (networkCapabilities.transportInfo is WifiInfo) {
+ networkCapabilities.transportInfo as WifiInfo
+ } else {
+ null
+ }
else -> null
}
}
+ /** True if these capabilities represent a wifi network. */
+ private fun isWifiNetwork(networkCapabilities: NetworkCapabilities): Boolean {
+ return when {
+ networkCapabilities.hasTransport(TRANSPORT_WIFI) -> true
+ networkCapabilities.hasTransport(TRANSPORT_CELLULAR) ->
+ Utils.tryGetWifiInfoForVcn(networkCapabilities) != null
+ else -> false
+ }
+ }
+
private fun createWifiNetworkModel(
wifiInfo: WifiInfo,
network: Network,
@@ -269,7 +292,19 @@
wifiManager: WifiManager,
): WifiNetworkModel {
return if (wifiInfo.isCarrierMerged) {
- WifiNetworkModel.CarrierMerged
+ if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) {
+ WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON)
+ } else {
+ WifiNetworkModel.CarrierMerged(
+ networkId = network.getNetId(),
+ subscriptionId = wifiInfo.subscriptionId,
+ level = wifiManager.calculateSignalLevel(wifiInfo.rssi),
+ // The WiFi signal level returned by WifiManager#calculateSignalLevel start
+ // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level
+ // buckets count.
+ numberOfLevels = wifiManager.maxSignalLevel + 1,
+ )
+ }
} else {
WifiNetworkModel.Active(
network.getNetId(),
@@ -302,6 +337,9 @@
.build()
private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel"
+
+ private const val CARRIER_MERGED_INVALID_SUB_ID_REASON =
+ "Wifi network was carrier merged but had invalid sub ID"
}
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 980560a..86dcd18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -66,6 +66,7 @@
override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
when (info) {
is WifiNetworkModel.Unavailable -> null
+ is WifiNetworkModel.Invalid -> null
is WifiNetworkModel.Inactive -> null
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Active -> when {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 824b597..95431af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -83,6 +83,7 @@
private fun WifiNetworkModel.icon(): WifiIcon {
return when (this) {
is WifiNetworkModel.Unavailable -> WifiIcon.Hidden
+ is WifiNetworkModel.Invalid -> WifiIcon.Hidden
is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden
is WifiNetworkModel.Inactive -> WifiIcon.Visible(
res = WIFI_NO_NETWORK,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 68d30d3..2b4f51c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -60,7 +60,7 @@
* animation to and from the parent dialog.
*/
@JvmOverloads
- open fun onUserListItemClicked(
+ fun onUserListItemClicked(
record: UserRecord,
dialogShower: DialogShower? = null,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
deleted file mode 100644
index 21a8300..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2016 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.statusbar.policy;
-
-import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.provider.Settings;
-import android.telephony.SubscriptionInfo;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.systemui.Dependency;
-
-import java.util.List;
-
-public class EmergencyCryptkeeperText extends TextView {
-
- private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
- @Override
- public void onPhoneStateChanged(int phoneState) {
- update();
- }
-
- @Override
- public void onRefreshCarrierInfo() {
- update();
- }
- };
- private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
- update();
- }
- }
- };
-
- public EmergencyCryptkeeperText(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- setVisibility(GONE);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
- mKeyguardUpdateMonitor.registerCallback(mCallback);
- getContext().registerReceiver(mReceiver,
- new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
- update();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mKeyguardUpdateMonitor != null) {
- mKeyguardUpdateMonitor.removeCallback(mCallback);
- }
- getContext().unregisterReceiver(mReceiver);
- }
-
- private boolean iccCardExist(int simState) {
- return ((simState == TelephonyManager.SIM_STATE_PIN_REQUIRED)
- || (simState == TelephonyManager.SIM_STATE_PUK_REQUIRED)
- || (simState == TelephonyManager.SIM_STATE_NETWORK_LOCKED)
- || (simState == TelephonyManager.SIM_STATE_READY)
- || (simState == TelephonyManager.SIM_STATE_NOT_READY)
- || (simState == TelephonyManager.SIM_STATE_PERM_DISABLED)
- || (simState == TelephonyManager.SIM_STATE_CARD_IO_ERROR)
- || (simState == TelephonyManager.SIM_STATE_CARD_RESTRICTED)
- || (simState == TelephonyManager.SIM_STATE_LOADED));
- }
-
- public void update() {
- boolean hasMobile = mContext.getSystemService(TelephonyManager.class).isDataCapable();
- boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.AIRPLANE_MODE_ON, 0) == 1);
-
- if (!hasMobile || airplaneMode) {
- setText(null);
- setVisibility(GONE);
- return;
- }
-
- boolean allSimsMissing = true;
- CharSequence displayText = null;
-
- List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getFilteredSubscriptionInfo();
- final int N = subs.size();
- for (int i = 0; i < N; i++) {
- int subId = subs.get(i).getSubscriptionId();
- int simState = mKeyguardUpdateMonitor.getSimState(subId);
- CharSequence carrierName = subs.get(i).getCarrierName();
- if (iccCardExist(simState) && !TextUtils.isEmpty(carrierName)) {
- allSimsMissing = false;
- displayText = carrierName;
- }
- }
- if (allSimsMissing) {
- if (N != 0) {
- // Shows "Emergency calls only" on devices that are voice-capable.
- // This depends on mPlmn containing the text "Emergency calls only" when the radio
- // has some connectivity. Otherwise it should show "No service"
- // Grab the first subscription, because they all should contain the emergency text,
- // described above.
- displayText = subs.get(0).getCarrierName();
- } else {
- // We don't have a SubscriptionInfo to get the emergency calls only from.
- // Grab it from the old sticky broadcast if possible instead. We can use it
- // here because no subscriptions are active, so we don't have
- // to worry about MSIM clashing.
- displayText = getContext().getText(
- com.android.internal.R.string.emergency_calls_only);
- Intent i = getContext().registerReceiver(null,
- new IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED));
- if (i != null) {
- displayText = i.getStringExtra(TelephonyManager.EXTRA_PLMN);
- }
- }
- }
-
- setText(displayText);
- setVisibility(TextUtils.isEmpty(displayText) ? GONE : VISIBLE);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EncryptionHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EncryptionHelper.java
deleted file mode 100644
index 9c099f9..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EncryptionHelper.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2016 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.statusbar.policy;
-
-import android.sysprop.VoldProperties;
-
-/**
- * Helper for determining whether the phone is decrypted yet.
- */
-public class EncryptionHelper {
-
- public static final boolean IS_DATA_ENCRYPTED = isDataEncrypted();
-
- private static boolean isDataEncrypted() {
- String voldState = VoldProperties.decrypt().orElse("");
- return "1".equals(voldState) || "trigger_restart_min_framework".equals(voldState);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
index 9946b4b..5dcafb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.policy;
import android.annotation.WorkerThread;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
@@ -255,7 +254,6 @@
setTorchMode(enabled);
mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
mSecureSettings.putInt(Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0);
- mBroadcastSender.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index f63d652..c8ee647 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -160,7 +160,7 @@
mStatusBarStateController = statusBarStateController;
mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView,
keyguardStateController, dozeParameters,
- screenOffAnimationController, /* animateYPos= */ false);
+ screenOffAnimationController, /* animateYPos= */ false, /* logBuffer= */ null);
mUserSwitchDialogController = userSwitchDialogController;
mUiEventLogger = uiEventLogger;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index c150654..e9f0dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -173,7 +173,7 @@
mUserSwitcherController, this);
mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView,
keyguardStateController, dozeParameters,
- screenOffAnimationController, /* animateYPos= */ false);
+ screenOffAnimationController, /* animateYPos= */ false, /* logBuffer= */ null);
mBackground = new KeyguardUserSwitcherScrim(context);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index c9ed0cb..4866f73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -109,6 +109,8 @@
private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33;
private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83;
private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f;
+ private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120;
+ private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180;
public final Object mToken = new Object();
@@ -301,7 +303,8 @@
mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null
&& editTextRootWindowInsets.isVisible(WindowInsets.Type.ime());
if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
- mController.removeRemoteInput(mEntry, mToken);
+ // Pass null to ensure all inputs are cleared for this entry b/227115380
+ mController.removeRemoteInput(mEntry, null);
}
}
}
@@ -421,7 +424,7 @@
}
@VisibleForTesting
- void onDefocus(boolean animate, boolean logClose) {
+ void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) {
mController.removeRemoteInput(mEntry, mToken);
mEntry.remoteInputText = mEditText.getText();
@@ -431,18 +434,20 @@
ViewGroup parent = (ViewGroup) getParent();
if (animate && parent != null && mIsFocusAnimationFlagActive) {
-
ViewGroup grandParent = (ViewGroup) parent.getParent();
ViewGroupOverlay overlay = parent.getOverlay();
+ View actionsContainer = getActionsContainerLayout();
+ int actionsContainerHeight =
+ actionsContainer != null ? actionsContainer.getHeight() : 0;
// After adding this RemoteInputView to the overlay of the parent (and thus removing
// it from the parent itself), the parent will shrink in height. This causes the
// overlay to be moved. To correct the position of the overlay we need to offset it.
- int overlayOffsetY = getMaxSiblingHeight() - getHeight();
+ int overlayOffsetY = actionsContainerHeight - getHeight();
overlay.add(this);
if (grandParent != null) grandParent.setClipChildren(false);
- Animator animator = getDefocusAnimator(overlayOffsetY);
+ Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY);
View self = this;
animator.addListener(new AnimatorListenerAdapter() {
@Override
@@ -454,8 +459,12 @@
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false);
}
+ if (doAfterDefocus != null) {
+ doAfterDefocus.run();
+ }
}
});
+ if (actionsContainer != null) actionsContainer.setAlpha(0f);
animator.start();
} else if (animate && mRevealParams != null && mRevealParams.radius > 0) {
@@ -474,6 +483,7 @@
reveal.start();
} else {
setVisibility(GONE);
+ if (doAfterDefocus != null) doAfterDefocus.run();
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false);
}
@@ -596,10 +606,8 @@
/**
* Focuses the RemoteInputView and animates its appearance
- *
- * @param crossFadeView view that will be crossfaded during the appearance animation
*/
- public void focusAnimated(View crossFadeView) {
+ public void focusAnimated() {
if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE
&& mRevealParams != null) {
android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this);
@@ -609,7 +617,7 @@
} else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) {
mIsAnimatingAppearance = true;
setAlpha(0f);
- Animator focusAnimator = getFocusAnimator(crossFadeView);
+ Animator focusAnimator = getFocusAnimator(getActionsContainerLayout());
focusAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
@@ -661,6 +669,23 @@
}
private void reset() {
+ if (mIsFocusAnimationFlagActive) {
+ mProgressBar.setVisibility(INVISIBLE);
+ mResetting = true;
+ mSending = false;
+ onDefocus(true /* animate */, false /* logClose */, () -> {
+ mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
+ mEditText.getText().clear();
+ mEditText.setEnabled(isAggregatedVisible());
+ mSendButton.setVisibility(VISIBLE);
+ mController.removeSpinning(mEntry.getKey(), mToken);
+ updateSendButton();
+ setAttachment(null);
+ mResetting = false;
+ });
+ return;
+ }
+
mResetting = true;
mSending = false;
mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
@@ -671,7 +696,7 @@
mProgressBar.setVisibility(INVISIBLE);
mController.removeSpinning(mEntry.getKey(), mToken);
updateSendButton();
- onDefocus(false /* animate */, false /* logClose */);
+ onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */);
setAttachment(null);
mResetting = false;
@@ -825,23 +850,22 @@
}
/**
- * @return max sibling height (0 in case of no siblings)
+ * @return action button container view (i.e. ViewGroup containing Reply button etc.)
*/
- public int getMaxSiblingHeight() {
+ public View getActionsContainerLayout() {
ViewGroup parentView = (ViewGroup) getParent();
- int maxHeight = 0;
- if (parentView == null) return 0;
- for (int i = 0; i < parentView.getChildCount(); i++) {
- View siblingView = parentView.getChildAt(i);
- if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight());
- }
- return maxHeight;
+ if (parentView == null) return null;
+ return parentView.findViewById(com.android.internal.R.id.actions_container_layout);
}
/**
* Creates an animator for the focus animation.
+ *
+ * @param fadeOutView View that will be faded out during the focus animation.
*/
- private Animator getFocusAnimator(View crossFadeView) {
+ private Animator getFocusAnimator(@Nullable View fadeOutView) {
+ final AnimatorSet animatorSet = new AnimatorSet();
+
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
@@ -854,30 +878,36 @@
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
- final Animator crossFadeViewAlphaAnimator =
- ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f);
- crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
- crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
- alphaAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation, boolean isReverse) {
- crossFadeView.setAlpha(1f);
- }
- });
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator);
+ if (fadeOutView == null) {
+ animatorSet.playTogether(alphaAnimator, scaleAnimator);
+ } else {
+ final Animator fadeOutViewAlphaAnimator =
+ ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f);
+ fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+ fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation, boolean isReverse) {
+ fadeOutView.setAlpha(1f);
+ }
+ });
+ animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator);
+ }
return animatorSet;
}
/**
* Creates an animator for the defocus animation.
*
- * @param offsetY The RemoteInputView will be offset by offsetY during the animation
+ * @param fadeInView View that will be faded in during the defocus animation.
+ * @param offsetY The RemoteInputView will be offset by offsetY during the animation
*/
- private Animator getDefocusAnimator(int offsetY) {
+ private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) {
+ final AnimatorSet animatorSet = new AnimatorSet();
+
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
- alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
+ alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
+ alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY);
alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
@@ -893,8 +923,17 @@
}
});
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(alphaAnimator, scaleAnimator);
+ if (fadeInView == null) {
+ animatorSet.playTogether(alphaAnimator, scaleAnimator);
+ } else {
+ fadeInView.forceHasOverlappingRendering(false);
+ Animator fadeInViewAlphaAnimator =
+ ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f);
+ fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION);
+ fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
+ fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY);
+ animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator);
+ }
return animatorSet;
}
@@ -1011,7 +1050,8 @@
if (isFocusable() && isEnabled()) {
setInnerFocusable(false);
if (mRemoteInputView != null) {
- mRemoteInputView.onDefocus(animate, true /* logClose */);
+ mRemoteInputView
+ .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */);
}
mShowImeOnInputConnection = false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
index e0d780a..7a4e35f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
@@ -48,6 +48,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
@@ -73,6 +74,7 @@
private boolean mIsAttached;
private final ViewGroup mStatusBarWindowView;
+ private final FragmentService mFragmentService;
// The container in which we should run launch animations started from the status bar and
// expanding into the opening window.
private final ViewGroup mLaunchAnimationContainer;
@@ -86,6 +88,7 @@
WindowManager windowManager,
IWindowManager iWindowManager,
StatusBarContentInsetsProvider contentInsetsProvider,
+ FragmentService fragmentService,
@Main Resources resources,
Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider) {
mContext = context;
@@ -93,6 +96,7 @@
mIWindowManager = iWindowManager;
mContentInsetsProvider = contentInsetsProvider;
mStatusBarWindowView = statusBarWindowView;
+ mFragmentService = fragmentService;
mLaunchAnimationContainer = mStatusBarWindowView.findViewById(
R.id.status_bar_launch_animation_container);
mLpChanged = new WindowManager.LayoutParams();
@@ -157,7 +161,7 @@
/** Returns a fragment host manager for the status bar window view. */
public FragmentHostManager getFragmentHostManager() {
- return FragmentHostManager.get(mStatusBarWindowView);
+ return mFragmentService.getFragmentHostManager(mStatusBarWindowView);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index 60f6df6..8f424b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -61,6 +61,10 @@
listeners.add(listener)
}
+ fun removeListener(listener: StatusBarWindowStateListener) {
+ listeners.remove(listener)
+ }
+
/** Returns true if the window is currently showing. */
fun windowIsShowing() = windowState == WINDOW_STATE_SHOWING
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt
deleted file mode 100644
index 154c6e2..0000000
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.stylus
-
-import android.content.Context
-import android.hardware.BatteryState
-import android.hardware.input.InputManager
-import android.os.Handler
-import android.util.Log
-import android.view.InputDevice
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-/**
- * A listener that detects when a stylus has first been used, by detecting 1) the presence of an
- * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth
- * address.
- */
-@SysUISingleton
-class StylusFirstUsageListener
-@Inject
-constructor(
- private val context: Context,
- private val inputManager: InputManager,
- private val stylusManager: StylusManager,
- private val featureFlags: FeatureFlags,
- @Background private val executor: Executor,
- @Background private val handler: Handler,
-) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
-
- // Set must be only accessed from the background handler, which is the same handler that
- // runs the StylusManager callbacks.
- private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf()
- @VisibleForTesting var hasStarted = false
-
- override fun start() {
- if (true) return // TODO(b/261826950): remove on main
- if (hasStarted) return
- if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
- if (inputManager.isStylusEverUsed(context)) return
- if (!hostDeviceSupportsStylusInput()) return
-
- hasStarted = true
- inputManager.inputDeviceIds.forEach(this::onStylusAdded)
- stylusManager.registerCallback(this)
- stylusManager.startListener()
- }
-
- override fun onStylusAdded(deviceId: Int) {
- if (!hasStarted) return
-
- val device = inputManager.getInputDevice(deviceId) ?: return
- if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return
-
- try {
- inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
- internalStylusDeviceIds += deviceId
- } catch (e: SecurityException) {
- Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.")
- }
- }
-
- override fun onStylusRemoved(deviceId: Int) {
- if (!hasStarted) return
-
- if (!internalStylusDeviceIds.contains(deviceId)) return
- try {
- inputManager.removeInputDeviceBatteryListener(deviceId, this)
- internalStylusDeviceIds.remove(deviceId)
- } catch (e: SecurityException) {
- Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
- }
- }
-
- override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
- if (!hasStarted) return
-
- onRemoteDeviceFound()
- }
-
- override fun onBatteryStateChanged(
- deviceId: Int,
- eventTimeMillis: Long,
- batteryState: BatteryState
- ) {
- if (!hasStarted) return
-
- if (batteryState.isPresent) {
- onRemoteDeviceFound()
- }
- }
-
- private fun onRemoteDeviceFound() {
- inputManager.setStylusEverUsed(context, true)
- cleanupListeners()
- }
-
- private fun cleanupListeners() {
- stylusManager.unregisterCallback(this)
- handler.post {
- internalStylusDeviceIds.forEach {
- inputManager.removeInputDeviceBatteryListener(it, this)
- }
- }
- }
-
- private fun hostDeviceSupportsStylusInput(): Boolean {
- return inputManager.inputDeviceIds
- .asSequence()
- .mapNotNull { inputManager.getInputDevice(it) }
- .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
- }
-
- companion object {
- private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 302d6a9..235495cf 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -18,6 +18,8 @@
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.util.ArrayMap
@@ -25,6 +27,8 @@
import android.view.InputDevice
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -37,25 +41,37 @@
class StylusManager
@Inject
constructor(
+ private val context: Context,
private val inputManager: InputManager,
private val bluetoothAdapter: BluetoothAdapter?,
@Background private val handler: Handler,
@Background private val executor: Executor,
-) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {
+ private val featureFlags: FeatureFlags,
+) :
+ InputManager.InputDeviceListener,
+ InputManager.InputDeviceBatteryListener,
+ BluetoothAdapter.OnMetadataChangedListener {
private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> =
CopyOnWriteArrayList()
// This map should only be accessed on the handler
private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()
+ // This variable should only be accessed on the handler
+ private var hasStarted: Boolean = false
/**
* Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
* at time of starting.
*/
fun startListener() {
- addExistingStylusToMap()
- inputManager.registerInputDeviceListener(this, handler)
+ handler.post {
+ if (hasStarted) return@post
+ hasStarted = true
+ addExistingStylusToMap()
+
+ inputManager.registerInputDeviceListener(this, handler)
+ }
}
/** Registers a StylusCallback to listen to stylus events. */
@@ -77,21 +93,30 @@
}
override fun onInputDeviceAdded(deviceId: Int) {
+ if (!hasStarted) return
+
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
+ if (!device.isExternal) {
+ registerBatteryListener(deviceId)
+ }
+
// TODO(b/257936830): get address once input api available
val btAddress: String? = null
inputDeviceAddressMap[deviceId] = btAddress
executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
if (btAddress != null) {
+ onStylusUsed()
onStylusBluetoothConnected(btAddress)
executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
}
}
override fun onInputDeviceChanged(deviceId: Int) {
+ if (!hasStarted) return
+
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
@@ -112,7 +137,10 @@
}
override fun onInputDeviceRemoved(deviceId: Int) {
+ if (!hasStarted) return
+
if (!inputDeviceAddressMap.contains(deviceId)) return
+ unregisterBatteryListener(deviceId)
val btAddress: String? = inputDeviceAddressMap[deviceId]
inputDeviceAddressMap.remove(deviceId)
@@ -124,13 +152,14 @@
}
override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
- handler.post executeMetadataChanged@{
- if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null)
- return@executeMetadataChanged
+ handler.post {
+ if (!hasStarted) return@post
+
+ if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post
val inputDeviceId: Int =
inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
- ?: return@executeMetadataChanged
+ ?: return@post
val isCharging = String(value) == "true"
@@ -140,6 +169,24 @@
}
}
+ override fun onBatteryStateChanged(
+ deviceId: Int,
+ eventTimeMillis: Long,
+ batteryState: BatteryState
+ ) {
+ handler.post {
+ if (!hasStarted) return@post
+
+ if (batteryState.isPresent) {
+ onStylusUsed()
+ }
+
+ executeStylusBatteryCallbacks { cb ->
+ cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState)
+ }
+ }
+ }
+
private fun onStylusBluetoothConnected(btAddress: String) {
val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
try {
@@ -158,6 +205,21 @@
}
}
+ /**
+ * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a
+ * physical stylus device has never been used. This method is run when 1) a USI stylus battery
+ * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a
+ * physical stylus device has actually been used.
+ */
+ private fun onStylusUsed() {
+ if (true) return // TODO(b/261826950): remove on main
+ if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
+ if (inputManager.isStylusEverUsed(context)) return
+
+ inputManager.setStylusEverUsed(context, true)
+ executeStylusCallbacks { cb -> cb.onStylusFirstUsed() }
+ }
+
private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
stylusCallbacks.forEach(run)
}
@@ -166,31 +228,69 @@
stylusBatteryCallbacks.forEach(run)
}
+ private fun registerBatteryListener(deviceId: Int) {
+ try {
+ inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
+ }
+ }
+
+ private fun unregisterBatteryListener(deviceId: Int) {
+ // If deviceId wasn't registered, the result is a no-op, so an "is registered"
+ // check is not needed.
+ try {
+ inputManager.removeInputDeviceBatteryListener(deviceId, this)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
+ }
+ }
+
private fun addExistingStylusToMap() {
for (deviceId: Int in inputManager.inputDeviceIds) {
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
// TODO(b/257936830): get address once input api available
inputDeviceAddressMap[deviceId] = null
+
+ if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
+ // For most devices, an active (non-bluetooth) stylus is represented by an
+ // internal InputDevice. This InputDevice will be present in InputManager
+ // before CoreStartables run, and will not be removed.
+ // In many cases, it reports the battery level of the stylus.
+ registerBatteryListener(deviceId)
+ }
}
}
}
- /** Callback interface to receive events from the StylusManager. */
+ /**
+ * Callback interface to receive events from the StylusManager. All callbacks are run on the
+ * same background handler.
+ */
interface StylusCallback {
fun onStylusAdded(deviceId: Int) {}
fun onStylusRemoved(deviceId: Int) {}
fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {}
fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
+ fun onStylusFirstUsed() {}
}
- /** Callback interface to receive stylus battery events from the StylusManager. */
+ /**
+ * Callback interface to receive stylus battery events from the StylusManager. All callbacks are
+ * runs on the same background handler.
+ */
interface StylusBatteryCallback {
fun onStylusBluetoothChargingStateChanged(
inputDeviceId: Int,
btDevice: BluetoothDevice,
isCharging: Boolean
) {}
+ fun onStylusUsiBatteryStateChanged(
+ deviceId: Int,
+ eventTimeMillis: Long,
+ batteryState: BatteryState,
+ ) {}
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
index 11233dd..dde2a80 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt
@@ -18,14 +18,11 @@
import android.hardware.BatteryState
import android.hardware.input.InputManager
-import android.util.Log
import android.view.InputDevice
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
-import java.util.concurrent.Executor
import javax.inject.Inject
/**
@@ -40,14 +37,16 @@
private val inputManager: InputManager,
private val stylusUsiPowerUi: StylusUsiPowerUI,
private val featureFlags: FeatureFlags,
- @Background private val executor: Executor,
-) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
+) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback {
override fun onStylusAdded(deviceId: Int) {
+ // On some devices, the addition of a new internal stylus indicates the use of a
+ // USI stylus with a different vendor/product ID. We would therefore like to reset
+ // the battery notification suppression, in case the user has dismissed a low battery
+ // notification of the previous stylus.
val device = inputManager.getInputDevice(deviceId) ?: return
-
if (!device.isExternal) {
- registerBatteryListener(deviceId)
+ stylusUsiPowerUi.updateSuppression(false)
}
}
@@ -59,57 +58,30 @@
stylusUsiPowerUi.refresh()
}
- override fun onStylusRemoved(deviceId: Int) {
- val device = inputManager.getInputDevice(deviceId) ?: return
-
- if (!device.isExternal) {
- unregisterBatteryListener(deviceId)
- }
- }
-
- override fun onBatteryStateChanged(
+ override fun onStylusUsiBatteryStateChanged(
deviceId: Int,
eventTimeMillis: Long,
batteryState: BatteryState
) {
- if (batteryState.isPresent) {
- stylusUsiPowerUi.updateBatteryState(batteryState)
- }
- }
-
- private fun registerBatteryListener(deviceId: Int) {
- try {
- inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
- } catch (e: SecurityException) {
- Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
- }
- }
-
- private fun unregisterBatteryListener(deviceId: Int) {
- try {
- inputManager.removeInputDeviceBatteryListener(deviceId, this)
- } catch (e: SecurityException) {
- Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.")
+ if (batteryState.isPresent && batteryState.capacity > 0f) {
+ stylusUsiPowerUi.updateBatteryState(deviceId, batteryState)
}
}
override fun start() {
if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return
- addBatteryListenerForInternalStyluses()
+ if (!hostDeviceSupportsStylusInput()) return
+ stylusUsiPowerUi.init()
stylusManager.registerCallback(this)
stylusManager.startListener()
}
- private fun addBatteryListenerForInternalStyluses() {
- // For most devices, an active stylus is represented by an internal InputDevice.
- // This InputDevice will be present in InputManager before CoreStartables run,
- // and will not be removed. In many cases, it reports the battery level of the stylus.
- inputManager.inputDeviceIds
+ private fun hostDeviceSupportsStylusInput(): Boolean {
+ return inputManager.inputDeviceIds
.asSequence()
.mapNotNull { inputManager.getInputDevice(it) }
- .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) }
- .forEach { onStylusAdded(it.id) }
+ .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
index 70a5b36..8d5e01c 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
@@ -18,17 +18,21 @@
import android.Manifest
import android.app.PendingIntent
+import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.BatteryState
import android.hardware.input.InputManager
+import android.os.Bundle
import android.os.Handler
import android.os.UserHandle
+import android.util.Log
import android.view.InputDevice
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -53,6 +57,7 @@
// These values must only be accessed on the handler.
private var batteryCapacity = 1.0f
private var suppressed = false
+ private var inputDeviceId: Int? = null
fun init() {
val filter =
@@ -87,10 +92,12 @@
}
}
- fun updateBatteryState(batteryState: BatteryState) {
+ fun updateBatteryState(deviceId: Int, batteryState: BatteryState) {
handler.post updateBattery@{
- if (batteryState.capacity == batteryCapacity) return@updateBattery
+ if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f)
+ return@updateBattery
+ inputDeviceId = deviceId
batteryCapacity = batteryState.capacity
refresh()
}
@@ -123,13 +130,13 @@
.setSmallIcon(R.drawable.ic_power_low)
.setDeleteIntent(getPendingBroadcast(ACTION_DISMISSED_LOW_BATTERY))
.setContentIntent(getPendingBroadcast(ACTION_CLICKED_LOW_BATTERY))
- .setContentTitle(context.getString(R.string.stylus_battery_low))
- .setContentText(
+ .setContentTitle(
context.getString(
- R.string.battery_low_percent_format,
+ R.string.stylus_battery_low_percentage,
NumberFormat.getPercentInstance().format(batteryCapacity)
)
)
+ .setContentText(context.getString(R.string.stylus_battery_low_subtitle))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setLocalOnly(true)
.setAutoCancel(true)
@@ -150,23 +157,41 @@
}
private fun getPendingBroadcast(action: String): PendingIntent? {
- return PendingIntent.getBroadcastAsUser(
+ return PendingIntent.getBroadcast(
context,
0,
- Intent(action),
+ Intent(action).setPackage(context.packageName),
PendingIntent.FLAG_IMMUTABLE,
- UserHandle.CURRENT
)
}
- private val receiver: BroadcastReceiver =
+ @VisibleForTesting
+ internal val receiver: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true)
ACTION_CLICKED_LOW_BATTERY -> {
updateSuppression(true)
- // TODO(b/261584943): open USI device details page
+ if (inputDeviceId == null) return
+
+ val args = Bundle()
+ args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!)
+ try {
+ context.startActivity(
+ Intent(ACTION_STYLUS_USI_DETAILS)
+ .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ } catch (e: ActivityNotFoundException) {
+ // In the rare scenario where the Settings app manifest doesn't contain
+ // the USI details activity, ignore the intent.
+ Log.e(
+ StylusUsiPowerUI::class.java.simpleName,
+ "Cannot open USI details page."
+ )
+ }
}
}
}
@@ -177,9 +202,13 @@
// https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41
private const val LOW_BATTERY_THRESHOLD = 0.16f
- private val USI_NOTIFICATION_ID = R.string.stylus_battery_low
+ private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage
- private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
- private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+ @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss"
+ @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click"
+ @VisibleForTesting
+ const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS"
+ @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id"
+ @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt
new file mode 100644
index 0000000..e092f01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.telephony.ui.activity
+
+import android.app.ActivityOptions
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.UserHandle
+import android.util.Log
+import android.view.WindowManager
+import com.android.internal.app.AlertActivity
+import com.android.systemui.R
+
+/** Dialog shown to the user to switch to managed profile for making a call using work SIM. */
+class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.OnClickListener {
+ private lateinit var phoneNumber: Uri
+ private var managedProfileUserId = UserHandle.USER_NULL
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ window.addSystemFlags(
+ WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
+ )
+ super.onCreate(savedInstanceState)
+
+ phoneNumber = intent.getData()
+ managedProfileUserId =
+ intent.getIntExtra(
+ "android.telecom.extra.MANAGED_PROFILE_USER_ID",
+ UserHandle.USER_NULL
+ )
+
+ mAlertParams.apply {
+ mTitle = getString(R.string.call_from_work_profile_title)
+ mMessage = getString(R.string.call_from_work_profile_text)
+ mPositiveButtonText = getString(R.string.call_from_work_profile_action)
+ mNegativeButtonText = getString(R.string.call_from_work_profile_close)
+ mPositiveButtonListener = this@SwitchToManagedProfileForCallActivity
+ mNegativeButtonListener = this@SwitchToManagedProfileForCallActivity
+ }
+ setupAlert()
+ }
+
+ override fun onClick(dialog: DialogInterface?, which: Int) {
+ if (which == BUTTON_POSITIVE) {
+ switchToManagedProfile()
+ }
+ finish()
+ }
+
+ private fun switchToManagedProfile() {
+ try {
+ applicationContext.startActivityAsUser(
+ Intent(Intent.ACTION_DIAL, phoneNumber),
+ ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(),
+ UserHandle.of(managedProfileUserId)
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to launch activity", e)
+ }
+ }
+
+ companion object {
+ private const val TAG = "SwitchToManagedProfileForCallActivity"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index ad48e21..1065d33 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -31,6 +31,7 @@
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
import androidx.annotation.CallSuper
+import androidx.annotation.VisibleForTesting
import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
import com.android.systemui.dagger.qualifiers.Main
@@ -108,9 +109,10 @@
* Whenever the current view disappears, the next-priority view will be displayed if it's still
* valid.
*/
+ @VisibleForTesting
internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
- private fun getCurrentDisplayInfo(): DisplayInfo? {
+ internal fun getCurrentDisplayInfo(): DisplayInfo? {
return activeViews.getOrNull(0)
}
@@ -119,15 +121,26 @@
dumpManager.registerNormalDumpable(this)
}
+ private val listeners: MutableSet<Listener> = mutableSetOf()
+
+ /** Registers a listener. */
+ fun registerListener(listener: Listener) {
+ listeners.add(listener)
+ }
+
+ /** Unregisters a listener. */
+ fun unregisterListener(listener: Listener) {
+ listeners.remove(listener)
+ }
+
/**
* Displays the view with the provided [newInfo].
*
* This method handles inflating and attaching the view, then delegates to [updateView] to
* display the correct information in the view.
- * @param onViewTimeout a runnable that runs after the view timeout.
*/
@Synchronized
- fun displayView(newInfo: T, onViewTimeout: Runnable? = null) {
+ fun displayView(newInfo: T) {
val timeout = accessibilityManager.getRecommendedTimeoutMillis(
newInfo.timeoutMs,
// Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
@@ -146,14 +159,13 @@
logger.logViewUpdate(newInfo)
currentDisplayInfo.info = newInfo
currentDisplayInfo.timeExpirationMillis = timeExpirationMillis
- updateTimeout(currentDisplayInfo, timeout, onViewTimeout)
+ updateTimeout(currentDisplayInfo, timeout)
updateView(newInfo, view)
return
}
val newDisplayInfo = DisplayInfo(
info = newInfo,
- onViewTimeout = onViewTimeout,
timeExpirationMillis = timeExpirationMillis,
// Null values will be updated to non-null if/when this view actually gets displayed
view = null,
@@ -196,7 +208,7 @@
private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) {
logger.logViewAddition(newDisplayInfo.info)
createAndAcquireWakeLock(newDisplayInfo)
- updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout)
+ updateTimeout(newDisplayInfo, timeout)
inflateAndUpdateView(newDisplayInfo)
}
@@ -227,19 +239,16 @@
/**
* Creates a runnable that will remove [displayInfo] in [timeout] ms from now.
*
- * @param onViewTimeout an optional runnable that will be run if the view times out.
* @return a runnable that, when run, will *cancel* the view's timeout.
*/
- private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) {
+ private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int) {
val cancelViewTimeout = mainExecutor.executeDelayed(
{
removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT)
- onViewTimeout?.run()
},
timeout.toLong()
)
- displayInfo.onViewTimeout = onViewTimeout
// Cancel old view timeout and re-set it.
displayInfo.cancelViewTimeout?.run()
displayInfo.cancelViewTimeout = cancelViewTimeout
@@ -317,6 +326,9 @@
// event comes in while this view is animating out, we still display the new view
// appropriately.
activeViews.remove(displayInfo)
+ listeners.forEach {
+ it.onInfoPermanentlyRemoved(id, removalReason)
+ }
// No need to time the view out since it's already gone
displayInfo.cancelViewTimeout?.run()
@@ -380,6 +392,9 @@
invalidViews.forEach {
activeViews.remove(it)
logger.logViewExpiration(it.info)
+ listeners.forEach { listener ->
+ listener.onInfoPermanentlyRemoved(it.info.id, REMOVAL_REASON_TIME_EXPIRED)
+ }
}
}
@@ -436,6 +451,15 @@
onAnimationEnd.run()
}
+ /** A listener interface to be notified of various view events. */
+ fun interface Listener {
+ /**
+ * Called whenever a [DisplayInfo] with the given [id] has been removed and will never be
+ * displayed again (unless another call to [updateView] is made).
+ */
+ fun onInfoPermanentlyRemoved(id: String, reason: String)
+ }
+
/** A container for all the display-related state objects. */
inner class DisplayInfo(
/**
@@ -461,11 +485,6 @@
var wakeLock: WakeLock?,
/**
- * See [displayView].
- */
- var onViewTimeout: Runnable?,
-
- /**
* A runnable that, when run, will cancel this view's timeout.
*
* Null if this info isn't currently being displayed.
@@ -475,6 +494,7 @@
}
private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
+private const val REMOVAL_REASON_TIME_EXPIRED = "TIMEOUT_EXPIRED_BEFORE_REDISPLAY"
private const val MIN_REQUIRED_TIME_FOR_REDISPLAY = 1000
private data class IconInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index ec6965a..899b0c2 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -80,6 +80,26 @@
)
}
+ /** Logs that there was a failure to animate the view in. */
+ fun logAnimateInFailure() {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {},
+ { "View's appearance animation failed. Forcing view display manually." },
+ )
+ }
+
+ /** Logs that there was a failure to animate the view out. */
+ fun logAnimateOutFailure() {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {},
+ { "View's disappearance animation failed." },
+ )
+ }
+
fun logViewHidden(info: T) {
buffer.log(
tag,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
new file mode 100644
index 0000000..01a81de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarAnimator.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.children
+import javax.inject.Inject
+
+/**
+ * A class controlling chipbar animations. Typically delegates to [ViewHierarchyAnimator].
+ *
+ * Used so that animations can be mocked out in tests.
+ */
+@SysUISingleton
+open class ChipbarAnimator @Inject constructor() {
+ /**
+ * Animates [innerView] and its children into view.
+ *
+ * @return true if the animation was successfully started and false if the animation can't be
+ * run for any reason.
+ *
+ * See [ViewHierarchyAnimator.animateAddition].
+ */
+ open fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ return ViewHierarchyAnimator.animateAddition(
+ innerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_DECELERATE,
+ duration = ANIMATION_IN_DURATION,
+ includeMargins = true,
+ includeFadeIn = true,
+ onAnimationEnd = onAnimationEnd,
+ )
+ }
+
+ /**
+ * Animates [innerView] and its children out of view.
+ *
+ * @return true if the animation was successfully started and false if the animation can't be
+ * run for any reason.
+ *
+ * See [ViewHierarchyAnimator.animateRemoval].
+ */
+ open fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ return ViewHierarchyAnimator.animateRemoval(
+ innerView,
+ ViewHierarchyAnimator.Hotspot.TOP,
+ Interpolators.EMPHASIZED_ACCELERATE,
+ ANIMATION_OUT_DURATION,
+ includeMargins = true,
+ onAnimationEnd,
+ )
+ }
+
+ /** Force shows this view and all child views. Should be used in case [animateViewIn] fails. */
+ fun forceDisplayView(innerView: View) {
+ innerView.alpha = 1f
+ if (innerView is ViewGroup) {
+ innerView.children.forEach { forceDisplayView(it) }
+ }
+ }
+}
+
+private const val ANIMATION_IN_DURATION = 500L
+private const val ANIMATION_OUT_DURATION = 250L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 04b1a50..696134c 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -32,8 +32,6 @@
import com.android.internal.widget.CachingIconView
import com.android.systemui.Gefingerpoken
import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.animation.ViewHierarchyAnimator
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -78,8 +76,10 @@
configurationController: ConfigurationController,
dumpManager: DumpManager,
powerManager: PowerManager,
+ private val chipbarAnimator: ChipbarAnimator,
private val falsingManager: FalsingManager,
private val falsingCollector: FalsingCollector,
+ private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
private val viewUtil: ViewUtil,
private val vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder,
@@ -105,6 +105,8 @@
commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) }
override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) {
+ updateGestureListening()
+
logger.logViewUpdate(
newInfo.windowTitle,
newInfo.text.loadText(context),
@@ -203,31 +205,67 @@
}
override fun animateViewIn(view: ViewGroup) {
- ViewHierarchyAnimator.animateAddition(
- view.getInnerView(),
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_DECELERATE,
- duration = ANIMATION_IN_DURATION,
- includeMargins = true,
- includeFadeIn = true,
- // We can only request focus once the animation finishes.
- onAnimationEnd = {
- maybeGetAccessibilityFocus(view.getTag(INFO_TAG) as ChipbarInfo?, view)
- },
- )
+ // We can only request focus once the animation finishes.
+ val onAnimationEnd = Runnable {
+ maybeGetAccessibilityFocus(view.getTag(INFO_TAG) as ChipbarInfo?, view)
+ }
+ val animatedIn = chipbarAnimator.animateViewIn(view.getInnerView(), onAnimationEnd)
+
+ // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run and the
+ // views would remain un-displayed. So, just force-set/run those items immediately.
+ if (!animatedIn) {
+ logger.logAnimateInFailure()
+ chipbarAnimator.forceDisplayView(view.getInnerView())
+ onAnimationEnd.run()
+ }
}
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
- ViewHierarchyAnimator.animateRemoval(
- innerView,
- ViewHierarchyAnimator.Hotspot.TOP,
- Interpolators.EMPHASIZED_ACCELERATE,
- ANIMATION_OUT_DURATION,
- includeMargins = true,
- onAnimationEnd,
- )
+ val removed = chipbarAnimator.animateViewOut(innerView, onAnimationEnd)
+ // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
+ // run it immediately.
+ if (!removed) {
+ logger.logAnimateOutFailure()
+ onAnimationEnd.run()
+ }
+
+ updateGestureListening()
+ }
+
+ private fun updateGestureListening() {
+ if (swipeChipbarAwayGestureHandler == null) {
+ return
+ }
+
+ val currentDisplayInfo = getCurrentDisplayInfo()
+ if (currentDisplayInfo != null && currentDisplayInfo.info.allowSwipeToDismiss) {
+ swipeChipbarAwayGestureHandler.setViewFetcher { currentDisplayInfo.view }
+ swipeChipbarAwayGestureHandler.addOnGestureDetectedCallback(TAG) {
+ onSwipeUpGestureDetected()
+ }
+ } else {
+ swipeChipbarAwayGestureHandler.resetViewFetcher()
+ swipeChipbarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
+ }
+ }
+
+ private fun onSwipeUpGestureDetected() {
+ val currentDisplayInfo = getCurrentDisplayInfo()
+ if (currentDisplayInfo == null) {
+ logger.logSwipeGestureError(id = null, errorMsg = "No info is being displayed")
+ return
+ }
+ if (!currentDisplayInfo.info.allowSwipeToDismiss) {
+ logger.logSwipeGestureError(
+ id = currentDisplayInfo.info.id,
+ errorMsg = "This view prohibits swipe-to-dismiss",
+ )
+ return
+ }
+ removeView(currentDisplayInfo.info.id, SWIPE_UP_GESTURE_REASON)
+ updateGestureListening()
}
private fun ViewGroup.getInnerView(): ViewGroup {
@@ -247,6 +285,6 @@
}
}
-private const val ANIMATION_IN_DURATION = 500L
-private const val ANIMATION_OUT_DURATION = 250L
@IdRes private val INFO_TAG = R.id.tag_chipbar_info
+private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
+private const val TAG = "ChipbarCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index dd4bd26..fe46318 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -33,12 +33,14 @@
* @property endItem an optional end item to display at the end of the chipbar (on the right in LTR
* locales; on the left in RTL locales).
* @property vibrationEffect an optional vibration effect when the chipbar is displayed
+ * @property allowSwipeToDismiss true if users are allowed to swipe up to dismiss this chipbar.
*/
data class ChipbarInfo(
val startIcon: TintedIcon,
val text: Text,
val endItem: ChipbarEndItem?,
val vibrationEffect: VibrationEffect? = null,
+ val allowSwipeToDismiss: Boolean = false,
override val windowTitle: String,
override val wakeReason: String,
override val timeoutMs: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
index fcfbe0a..f239428 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
@@ -46,4 +46,16 @@
{ "Chipbar updated. window=$str1 text=$str2 endItem=$str3" }
)
}
+
+ fun logSwipeGestureError(id: String?, errorMsg: String) {
+ buffer.log(
+ tag,
+ LogLevel.WARNING,
+ {
+ str1 = id
+ str2 = errorMsg
+ },
+ { "Chipbar swipe gesture detected for incorrect state. id=$str1 error=$str2" }
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
new file mode 100644
index 0000000..9dbc4b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.content.Context
+import android.view.MotionEvent
+import android.view.View
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.statusbar.gesture.SwipeUpGestureHandler
+import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
+import com.android.systemui.util.boundsOnScreen
+
+/**
+ * A class to detect when a user has swiped the chipbar away.
+ *
+ * Effectively [SysUISingleton]. But, this shouldn't be created if the gesture isn't enabled. See
+ * [TemporaryDisplayModule.provideSwipeChipbarAwayGestureHandler].
+ */
+class SwipeChipbarAwayGestureHandler(
+ context: Context,
+ displayTracker: DisplayTracker,
+ logger: SwipeUpGestureLogger,
+) : SwipeUpGestureHandler(context, displayTracker, logger, loggerTag = LOGGER_TAG) {
+
+ private var viewFetcher: () -> View? = { null }
+
+ override fun startOfGestureIsWithinBounds(ev: MotionEvent): Boolean {
+ val view = viewFetcher.invoke() ?: return false
+ // Since chipbar is in its own window, we need to use [boundsOnScreen] to get an accurate
+ // bottom. ([view.bottom] would be relative to its window, which would be too small.)
+ val viewBottom = view.boundsOnScreen.bottom
+ // Allow the gesture to start a bit below the chipbar
+ return ev.y <= 1.5 * viewBottom
+ }
+
+ /**
+ * Sets a fetcher that returns the current chipbar view. The fetcher will be invoked whenever a
+ * gesture starts to determine if the gesture is near the chipbar.
+ */
+ fun setViewFetcher(fetcher: () -> View?) {
+ viewFetcher = fetcher
+ }
+
+ /** Removes the current view fetcher. */
+ fun resetViewFetcher() {
+ viewFetcher = { null }
+ }
+}
+
+private const val LOGGER_TAG = "SwipeChipbarAway"
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
index cf0a183..b1be404 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
@@ -16,22 +16,40 @@
package com.android.systemui.temporarydisplay.dagger
+import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
+import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
import dagger.Module
import dagger.Provides
@Module
interface TemporaryDisplayModule {
- @Module
companion object {
- @JvmStatic
@Provides
@SysUISingleton
@ChipbarLog
fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("ChipbarLog", 40)
}
+
+ @Provides
+ @SysUISingleton
+ fun provideSwipeChipbarAwayGestureHandler(
+ mediaTttFlags: MediaTttFlags,
+ context: Context,
+ displayTracker: DisplayTracker,
+ logger: SwipeUpGestureLogger,
+ ): SwipeChipbarAwayGestureHandler? {
+ return if (mediaTttFlags.isMediaTttDismissGestureEnabled()) {
+ SwipeChipbarAwayGestureHandler(context, displayTracker, logger)
+ } else {
+ null
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 3ecb15b..3e3a891 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -26,7 +26,6 @@
import static com.android.systemui.theme.ThemeOverlayApplier.OVERLAY_COLOR_SOURCE;
import static com.android.systemui.theme.ThemeOverlayApplier.TIMESTAMP_FIELD;
-import android.annotation.Nullable;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
import android.app.WallpaperManager.OnColorsChangedListener;
@@ -70,6 +69,7 @@
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.monet.Style;
+import com.android.systemui.monet.TonalPalette;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
@@ -357,13 +357,22 @@
};
@Inject
- public ThemeOverlayController(Context context, BroadcastDispatcher broadcastDispatcher,
- @Background Handler bgHandler, @Main Executor mainExecutor,
- @Background Executor bgExecutor, ThemeOverlayApplier themeOverlayApplier,
- SecureSettings secureSettings, WallpaperManager wallpaperManager,
- UserManager userManager, DeviceProvisionedController deviceProvisionedController,
- UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
- @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
+ public ThemeOverlayController(
+ Context context,
+ BroadcastDispatcher broadcastDispatcher,
+ @Background Handler bgHandler,
+ @Main Executor mainExecutor,
+ @Background Executor bgExecutor,
+ ThemeOverlayApplier themeOverlayApplier,
+ SecureSettings secureSettings,
+ WallpaperManager wallpaperManager,
+ UserManager userManager,
+ DeviceProvisionedController deviceProvisionedController,
+ UserTracker userTracker,
+ DumpManager dumpManager,
+ FeatureFlags featureFlags,
+ @Main Resources resources,
+ WakefulnessLifecycle wakefulnessLifecycle) {
mContext = context;
mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES);
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
@@ -391,7 +400,7 @@
mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, mMainExecutor,
UserHandle.ALL);
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES),
+ Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
false,
new ContentObserver(mBgHandler) {
@Override
@@ -513,39 +522,42 @@
/**
* Given a color candidate, return an overlay definition.
*/
- protected @Nullable FabricatedOverlay getOverlay(int color, int type, Style style) {
+ protected FabricatedOverlay getOverlay(int color, int type, Style style) {
boolean nightMode = (mResources.getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
mColorScheme = new ColorScheme(color, nightMode, style);
- List<Integer> colorShades = type == ACCENT
- ? mColorScheme.getAllAccentColors() : mColorScheme.getAllNeutralColors();
String name = type == ACCENT ? "accent" : "neutral";
- int paletteSize = mColorScheme.getAccent1().size();
+
FabricatedOverlay.Builder overlay =
new FabricatedOverlay.Builder("com.android.systemui", name, "android");
- for (int i = 0; i < colorShades.size(); i++) {
- int luminosity = i % paletteSize;
- int paletteIndex = i / paletteSize + 1;
- String resourceName;
- switch (luminosity) {
- case 0:
- resourceName = "android:color/system_" + name + paletteIndex + "_10";
- break;
- case 1:
- resourceName = "android:color/system_" + name + paletteIndex + "_50";
- break;
- default:
- int l = luminosity - 1;
- resourceName = "android:color/system_" + name + paletteIndex + "_" + l + "00";
- }
- overlay.setResourceValue(resourceName, TypedValue.TYPE_INT_COLOR_ARGB8,
- ColorUtils.setAlphaComponent(colorShades.get(i), 0xFF));
+
+ if (type == ACCENT) {
+ assignTonalPaletteToOverlay("accent1", overlay, mColorScheme.getAccent1());
+ assignTonalPaletteToOverlay("accent2", overlay, mColorScheme.getAccent2());
+ assignTonalPaletteToOverlay("accent3", overlay, mColorScheme.getAccent3());
+ } else {
+ assignTonalPaletteToOverlay("neutral1", overlay, mColorScheme.getNeutral1());
+ assignTonalPaletteToOverlay("neutral2", overlay, mColorScheme.getNeutral2());
}
return overlay.build();
}
+ private void assignTonalPaletteToOverlay(String name,
+ FabricatedOverlay.Builder overlay, TonalPalette tonalPalette) {
+
+ String resourcePrefix = "android:color/system_" + name;
+ int colorDataType = TypedValue.TYPE_INT_COLOR_ARGB8;
+
+ tonalPalette.getAllShadesMapped().forEach((key, value) -> {
+ String resourceName = resourcePrefix + "_" + key;
+ int colorValue = ColorUtils.setAlphaComponent(value, 0xFF);
+ overlay.setResourceValue(resourceName, colorDataType,
+ colorValue);
+ });
+ }
+
/**
* Checks if the color scheme in mColorScheme matches the current system palettes.
* @param managedProfiles List of managed profiles for this user.
@@ -557,15 +569,15 @@
Resources res = userHandle.isSystem()
? mResources : mContext.createContextAsUser(userHandle, 0).getResources();
if (!(res.getColor(android.R.color.system_accent1_500, mContext.getTheme())
- == mColorScheme.getAccent1().get(6)
+ == mColorScheme.getAccent1().getS500()
&& res.getColor(android.R.color.system_accent2_500, mContext.getTheme())
- == mColorScheme.getAccent2().get(6)
+ == mColorScheme.getAccent2().getS500()
&& res.getColor(android.R.color.system_accent3_500, mContext.getTheme())
- == mColorScheme.getAccent3().get(6)
+ == mColorScheme.getAccent3().getS500()
&& res.getColor(android.R.color.system_neutral1_500, mContext.getTheme())
- == mColorScheme.getNeutral1().get(6)
+ == mColorScheme.getNeutral1().getS500()
&& res.getColor(android.R.color.system_neutral2_500, mContext.getTheme())
- == mColorScheme.getNeutral2().get(6))) {
+ == mColorScheme.getNeutral2().getS500())) {
return false;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
index 79811c5..2475890 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/RadioListPreference.java
@@ -28,8 +28,9 @@
import androidx.preference.PreferenceScreen;
import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
-import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.fragments.FragmentService;
import java.util.Objects;
@@ -74,7 +75,7 @@
RadioFragment f = new RadioFragment();
f.setPreference(this);
- FragmentHostManager.get(v).getFragmentManager()
+ Dependency.get(FragmentService.class).getFragmentHostManager(v).getFragmentManager()
.beginTransaction()
.add(android.R.id.content, f)
.commit();
@@ -86,8 +87,10 @@
Bundle savedInstanceState) {
super.onDialogStateRestored(fragment, dialog, savedInstanceState);
View view = dialog.findViewById(R.id.content);
- RadioFragment radioFragment = (RadioFragment) FragmentHostManager.get(view)
- .getFragmentManager().findFragmentById(R.id.content);
+ RadioFragment radioFragment = (RadioFragment) Dependency.get(FragmentService.class)
+ .getFragmentHostManager(view)
+ .getFragmentManager()
+ .findFragmentById(R.id.content);
if (radioFragment != null) {
radioFragment.setPreference(this);
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
index 7726d09..8214822 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
@@ -3,26 +3,43 @@
import android.os.SystemProperties
import android.os.VibrationEffect
import android.os.Vibrator
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
+import java.util.concurrent.Executor
import javax.inject.Inject
-/**
- * Class that plays a haptics effect during unfolding a foldable device
- */
+/** Class that plays a haptics effect during unfolding a foldable device */
@SysUIUnfoldScope
class UnfoldHapticsPlayer
@Inject
constructor(
unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+ foldProvider: FoldProvider,
+ @Main private val mainExecutor: Executor,
private val vibrator: Vibrator?
) : TransitionProgressListener {
+ private var isFirstAnimationAfterUnfold = false
+
init {
if (vibrator != null) {
// We don't need to remove the callback because we should listen to it
// the whole time when SystemUI process is alive
unfoldTransitionProgressProvider.addCallback(this)
}
+
+ foldProvider.registerCallback(
+ object : FoldCallback {
+ override fun onFoldUpdated(isFolded: Boolean) {
+ if (isFolded) {
+ isFirstAnimationAfterUnfold = true
+ }
+ }
+ },
+ mainExecutor
+ )
}
private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN
@@ -36,6 +53,13 @@
}
override fun onTransitionFinishing() {
+ // Run haptics only when unfolding the device (first animation after unfolding)
+ if (!isFirstAnimationAfterUnfold) {
+ return
+ }
+
+ isFirstAnimationAfterUnfold = false
+
// Run haptics only if the animation is long enough to notice
if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) {
playHaptics()
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 523cf68..19a0866 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -36,6 +36,9 @@
import android.view.WindowManager
import android.view.WindowlessWindowManager
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.DisplayTracker
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
@@ -57,6 +60,7 @@
@Inject
constructor(
private val context: Context,
+ private val featureFlags: FeatureFlags,
private val deviceStateManager: DeviceStateManager,
private val contentResolver: ContentResolver,
private val displayManager: DisplayManager,
@@ -65,6 +69,7 @@
@Main private val executor: Executor,
private val threadFactory: ThreadFactory,
private val rotationChangeProvider: RotationChangeProvider,
+ private val displayTracker: DisplayTracker
) {
private val transitionListener = TransitionListener()
@@ -81,6 +86,7 @@
private var scrimView: LightRevealScrim? = null
private var isFolded: Boolean = false
private var isUnfoldHandled: Boolean = true
+ private var overlayAddReason: AddOverlayReason? = null
private var currentRotation: Int = context.display!!.rotation
@@ -100,7 +106,7 @@
.setName("unfold-overlay-container")
displayAreaHelper.get().attachToRootDisplayArea(
- Display.DEFAULT_DISPLAY,
+ displayTracker.defaultDisplayId,
containerBuilder
) { builder ->
executor.execute {
@@ -158,6 +164,8 @@
ensureInBackground()
ensureOverlayRemoved()
+ overlayAddReason = reason
+
val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
val params = getLayoutParams()
val newView =
@@ -170,11 +178,7 @@
.apply {
revealEffect = createLightRevealEffect()
isScrimOpaqueChangedListener = Consumer {}
- revealAmount =
- when (reason) {
- FOLD -> TRANSPARENT
- UNFOLD -> BLACK
- }
+ revealAmount = calculateRevealAmount()
}
newRoot.setView(newView, params)
@@ -207,6 +211,31 @@
root = newRoot
}
+ private fun calculateRevealAmount(animationProgress: Float? = null): Float {
+ val overlayAddReason = overlayAddReason ?: UNFOLD
+
+ if (animationProgress == null) {
+ // Animation progress is unknown, calculate the initial value based on the overlay
+ // add reason
+ return when (overlayAddReason) {
+ FOLD -> TRANSPARENT
+ UNFOLD -> BLACK
+ }
+ }
+
+ val showVignetteWhenFolding =
+ featureFlags.isEnabled(Flags.ENABLE_DARK_VIGNETTE_WHEN_FOLDING)
+
+ return if (!showVignetteWhenFolding && overlayAddReason == FOLD) {
+ // Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off
+ // and we are folding the device. We still add the overlay to block touches
+ // while the animation is running but the overlay is transparent.
+ TRANSPARENT
+ } else {
+ animationProgress
+ }
+ }
+
private fun getLayoutParams(): WindowManager.LayoutParams {
val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
@@ -259,7 +288,7 @@
private inner class TransitionListener : TransitionProgressListener {
override fun onTransitionProgress(progress: Float) {
- executeInBackground { scrimView?.revealAmount = progress }
+ executeInBackground { scrimView?.revealAmount = calculateRevealAmount(progress) }
}
override fun onTransitionFinished() {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index 59ad24a..2709da3 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -17,6 +17,9 @@
package com.android.systemui.unfold
import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.LifecycleScreenStatusProvider
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.system.SystemUnfoldSharedModule
@@ -32,6 +35,7 @@
import dagger.Module
import dagger.Provides
import java.util.Optional
+import java.util.concurrent.Executor
import javax.inject.Named
import javax.inject.Singleton
@@ -40,6 +44,20 @@
@Provides @UnfoldTransitionATracePrefix fun tracingTagPrefix() = "systemui"
+ /** A globally available FoldStateListener that allows one to query the fold state. */
+ @Provides
+ @Singleton
+ fun providesFoldStateListener(
+ deviceStateManager: DeviceStateManager,
+ @Application context: Context,
+ @Main executor: Executor
+ ): DeviceStateManager.FoldStateListener {
+ val listener = DeviceStateManager.FoldStateListener(context)
+ deviceStateManager.registerCallback(executor, listener)
+
+ return listener
+ }
+
@Provides
@Singleton
fun providesFoldStateLoggingProvider(
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index c0f0390..e5ab473 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -17,8 +17,11 @@
package com.android.systemui.user.data.repository
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.Context
import android.content.pm.UserInfo
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
@@ -30,6 +33,8 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.GlobalSettings
@@ -68,6 +73,9 @@
/** [UserInfo] of the currently-selected user. */
val selectedUserInfo: Flow<UserInfo>
+ /** Whether user switching is currently in progress. */
+ val userSwitchingInProgress: Flow<Boolean>
+
/** User ID of the last non-guest selected user. */
val lastSelectedNonGuestUserId: Int
@@ -108,6 +116,8 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val globalSettings: GlobalSettings,
private val tracker: UserTracker,
+ private val activityManager: IActivityManager,
+ featureFlags: FeatureFlags,
) : UserRepository {
private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() })
@@ -129,6 +139,10 @@
private var _isGuestUserResetting: Boolean = false
override var isGuestUserResetting: Boolean = _isGuestUserResetting
+ private val _isUserSwitchingInProgress = MutableStateFlow(false)
+ override val userSwitchingInProgress: Flow<Boolean>
+ get() = _isUserSwitchingInProgress
+
override val isGuestUserCreationScheduled = AtomicBoolean()
override val isStatusBarUserChipEnabled: Boolean =
@@ -141,6 +155,9 @@
init {
observeSelectedUser()
observeUserSettings()
+ if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) {
+ observeUserSwitching()
+ }
}
override fun refreshUsers() {
@@ -166,6 +183,28 @@
return _userSwitcherSettings.value.isSimpleUserSwitcher
}
+ private fun observeUserSwitching() {
+ conflatedCallbackFlow {
+ val callback =
+ object : UserSwitchObserver() {
+ override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback) {
+ trySendWithFailureLogging(true, TAG, "userSwitching started")
+ }
+
+ override fun onUserSwitchComplete(newUserId: Int) {
+ trySendWithFailureLogging(false, TAG, "userSwitching completed")
+ }
+ }
+ activityManager.registerUserSwitchObserver(callback, TAG)
+ trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
+ awaitClose { activityManager.unregisterUserSwitchObserver(callback) }
+ }
+ .onEach { _isUserSwitchingInProgress.value = it }
+ // TODO (b/262838215), Make this stateIn and initialize directly in field declaration
+ // once the flag is launched
+ .launchIn(applicationScope)
+ }
+
private fun observeSelectedUser() {
conflatedCallbackFlow {
fun send() {
@@ -174,7 +213,7 @@
val callback =
object : UserTracker.Callback {
- override fun onUserChanged(newUser: Int, userContext: Context) {
+ override fun onUserChanging(newUser: Int, userContext: Context) {
send()
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index d7b0971..c0ba3cc 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -115,9 +115,7 @@
private val callbackMutex = Mutex()
private val callbacks = mutableSetOf<UserCallback>()
private val userInfos: Flow<List<UserInfo>> =
- repository.userInfos.map { userInfos ->
- userInfos.filter { it.isFull }
- }
+ repository.userInfos.map { userInfos -> userInfos.filter { it.isFull } }
/** List of current on-device users to select from. */
val users: Flow<List<UserModel>>
@@ -445,7 +443,8 @@
)
)
}
- UserActionModel.ADD_SUPERVISED_USER ->
+ UserActionModel.ADD_SUPERVISED_USER -> {
+ dismissDialog()
activityStarter.startActivity(
Intent()
.setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
@@ -453,6 +452,7 @@
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
/* dismissShade= */ true,
)
+ }
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
activityStarter.startActivity(
Intent(Settings.ACTION_USER_SETTINGS),
@@ -493,7 +493,7 @@
fun showUserSwitcher(context: Context, expandable: Expandable) {
if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
- showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog)
+ showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable))
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
index 85c2964..14cc3e7 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -18,11 +18,13 @@
package com.android.systemui.user.domain.model
import android.os.UserHandle
+import com.android.systemui.animation.Expandable
import com.android.systemui.qs.user.UserSwitchDialogController
/** Encapsulates a request to show a dialog. */
sealed class ShowDialogRequestModel(
open val dialogShower: UserSwitchDialogController.DialogShower? = null,
+ open val expandable: Expandable? = null,
) {
data class ShowAddUserDialog(
val userHandle: UserHandle,
@@ -45,5 +47,7 @@
) : ShowDialogRequestModel(dialogShower)
/** Show the user switcher dialog */
- object ShowUserSwitcherDialog : ShowDialogRequestModel()
+ data class ShowUserSwitcherDialog(
+ override val expandable: Expandable?,
+ ) : ShowDialogRequestModel()
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
index a9d66de..5a25a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
@@ -63,11 +63,10 @@
}
// Use broadcast instead of ShadeController, as this dialog may have started in
- // another
- // process where normal dagger bindings are not available.
+ // another process where normal dagger bindings are not available.
broadcastSender.sendBroadcastAsUser(
Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
- UserHandle.CURRENT
+ userHandle
)
context.startActivityAsUser(
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt
new file mode 100644
index 0000000..3fe2a7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/DialogShowerImpl.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.ui.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
+
+/** Extracted from [UserSwitchDialogController] */
+class DialogShowerImpl(
+ private val animateFrom: Dialog,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+) : DialogInterface by animateFrom, DialogShower {
+ override fun showDialog(dialog: Dialog, cuj: DialogCuj) {
+ dialogLaunchAnimator.showFromDialog(dialog, animateFrom = animateFrom, cuj)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
index ed25898..b8ae257 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
@@ -60,6 +60,7 @@
setView(gridFrame)
adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
+ adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator))
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 4141054..79721b3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -66,12 +66,6 @@
private fun startHandlingDialogShowRequests() {
applicationScope.get().launch {
interactor.get().dialogShowRequests.filterNotNull().collect { request ->
- currentDialog?.let {
- if (it.isShowing) {
- it.cancel()
- }
- }
-
val (dialog, dialogCuj) =
when (request) {
is ShowDialogRequestModel.ShowAddUserDialog ->
@@ -133,7 +127,10 @@
}
currentDialog = dialog
- if (request.dialogShower != null && dialogCuj != null) {
+ val controller = request.expandable?.dialogLaunchController(dialogCuj)
+ if (controller != null) {
+ dialogLaunchAnimator.get().show(dialog, controller)
+ } else if (request.dialogShower != null && dialogCuj != null) {
request.dialogShower?.showDialog(dialog, dialogCuj)
} else {
dialog.show()
diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java
index d54de3fa..c2727fc 100644
--- a/packages/SystemUI/src/com/android/systemui/util/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java
@@ -14,8 +14,6 @@
package com.android.systemui.util;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import android.Manifest;
import android.content.Context;
import android.content.Intent;
@@ -26,6 +24,7 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.shared.system.QuickStepContract;
import java.util.List;
@@ -71,8 +70,9 @@
* {@link android.view.WindowManagerPolicyConstants#NAV_BAR_MODE_GESTURAL} AND
* the context is that of the default display
*/
- public static boolean isGesturalModeOnDefaultDisplay(Context context, int navMode) {
- return context.getDisplayId() == DEFAULT_DISPLAY
+ public static boolean isGesturalModeOnDefaultDisplay(Context context,
+ DisplayTracker displayTracker, int navMode) {
+ return context.getDisplayId() == displayTracker.getDefaultDisplayId()
&& QuickStepContract.isGesturalMode(navMode);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
index 08ee0af..56c5d3b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
@@ -27,6 +27,8 @@
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.animation.LaunchableView
+import com.android.systemui.animation.LaunchableViewDelegate
import com.android.systemui.statusbar.CrossFadeHelper
/**
@@ -38,7 +40,7 @@
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
-) : ConstraintLayout(context, attrs, defStyleAttr) {
+) : ConstraintLayout(context, attrs, defStyleAttr), LaunchableView {
private val boundsRect = Rect()
private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
@@ -50,7 +52,11 @@
private var desiredMeasureWidth = 0
private var desiredMeasureHeight = 0
- private var transitionVisibility = View.VISIBLE
+ private val delegate =
+ LaunchableViewDelegate(
+ this,
+ superSetVisibility = { super.setVisibility(it) },
+ )
/**
* The measured state of this view which is the one we will lay ourselves out with. This
@@ -83,11 +89,12 @@
}
}
- override fun setTransitionVisibility(visibility: Int) {
- // We store the last transition visibility assigned to this view to restore it later if
- // necessary.
- super.setTransitionVisibility(visibility)
- transitionVisibility = visibility
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {
+ delegate.setShouldBlockVisibilityChanges(block)
+ }
+
+ override fun setVisibility(visibility: Int) {
+ delegate.setVisibility(visibility)
}
override fun onFinishInflate() {
@@ -173,14 +180,6 @@
translationY = currentState.translation.y
CrossFadeHelper.fadeIn(this, currentState.alpha)
-
- // CrossFadeHelper#fadeIn will change this view visibility, which overrides the transition
- // visibility. We set the transition visibility again to make sure that this view plays well
- // with GhostView, which sets the transition visibility and is used for activity launch
- // animations.
- if (transitionVisibility != View.VISIBLE) {
- setTransitionVisibility(transitionVisibility)
- }
}
private fun applyCurrentStateOnPredraw() {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index b61b2e6..47d505e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -16,14 +16,21 @@
package com.android.systemui.util.kotlin
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.SystemClockImpl
+import kotlinx.coroutines.CoroutineStart
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
+import kotlin.math.max
/**
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
@@ -167,3 +174,89 @@
* Note that the returned Flow will not emit anything until [other] has emitted at least one value.
*/
fun <A> Flow<*>.sample(other: Flow<A>): Flow<A> = sample(other) { _, a -> a }
+
+/**
+ * Returns a flow that mirrors the original flow, but delays values following emitted values for the
+ * given [periodMs]. If the original flow emits more than one value during this period, only the
+ * latest value is emitted.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * flow {
+ * emit(1) // t=0ms
+ * delay(90)
+ * emit(2) // t=90ms
+ * delay(90)
+ * emit(3) // t=180ms
+ * delay(1010)
+ * emit(4) // t=1190ms
+ * delay(1010)
+ * emit(5) // t=2200ms
+ * }.throttle(1000)
+ * ```
+ *
+ * produces the following emissions at the following times
+ *
+ * ```text
+ * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
+ * ```
+ */
+fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, SystemClockImpl())
+
+/**
+ * Returns a flow that mirrors the original flow, but delays values following emitted values for the
+ * given [periodMs] as reported by the given [clock]. If the original flow emits more than one value
+ * during this period, only The latest value is emitted.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * flow {
+ * emit(1) // t=0ms
+ * delay(90)
+ * emit(2) // t=90ms
+ * delay(90)
+ * emit(3) // t=180ms
+ * delay(1010)
+ * emit(4) // t=1190ms
+ * delay(1010)
+ * emit(5) // t=2200ms
+ * }.throttle(1000)
+ * ```
+ *
+ * produces the following emissions at the following times
+ *
+ * ```text
+ * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
+ * ```
+ */
+fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock): Flow<T> = channelFlow {
+ coroutineScope {
+ var previousEmitTimeMs = 0L
+ var delayJob: Job? = null
+ var sendJob: Job? = null
+ val outerScope = this
+
+ collect {
+ delayJob?.cancel()
+ sendJob?.join()
+ val currentTimeMs = clock.elapsedRealtime()
+ val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
+ val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
+ if (timeUntilNextEmit > 0L) {
+ // We create delayJob to allow cancellation during the delay period
+ delayJob = launch {
+ delay(timeUntilNextEmit)
+ sendJob = outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ send(it)
+ previousEmitTimeMs = clock.elapsedRealtime()
+ }
+ }
+ } else {
+ send(it)
+ previousEmitTimeMs = currentTimeMs
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
index 4351afe..a0d22f3 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
@@ -29,12 +29,12 @@
import android.net.Uri;
import android.os.Debug;
import android.os.SystemProperties;
-import android.os.UserHandle;
import android.util.Log;
import androidx.core.content.FileProvider;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
import com.google.android.collect.Lists;
@@ -62,13 +62,15 @@
static final String LEAK_DUMP = "leak.dump";
private final Context mContext;
+ private final UserTracker mUserTracker;
private final LeakDetector mLeakDetector;
private final String mLeakReportEmail;
@Inject
- public LeakReporter(Context context, LeakDetector leakDetector,
+ public LeakReporter(Context context, UserTracker userTracker, LeakDetector leakDetector,
@Nullable @Named(LEAK_REPORT_EMAIL_NAME) String leakReportEmail) {
mContext = context;
+ mUserTracker = userTracker;
mLeakDetector = leakDetector;
mLeakReportEmail = leakReportEmail;
}
@@ -111,7 +113,7 @@
getIntent(hprofFile, dumpFile),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE,
null,
- UserHandle.CURRENT));
+ mUserTracker.getUserHandle()));
notiMan.notify(TAG, 0, builder.build());
} catch (IOException e) {
Log.e(TAG, "Couldn't dump heap for leak", e);
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java b/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java
index 0b2f004..31f35fc 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java
@@ -1,15 +1,17 @@
/*
* Copyright (C) 2017 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
*
- * Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package com.android.systemui.util.leak;
@@ -26,7 +28,27 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-public class RotationUtils {
+/**
+ * Utility class that provides device orientation.
+ *
+ * <p>Consider using {@link Surface.Rotation} or add a function that respects device aspect ratio
+ * and {@code android.internal.R.bool.config_reverseDefaultRotation}.
+ *
+ * <p>If you only care about the rotation, use {@link Surface.Rotation}, as it always gives the
+ * counter clock-wise rotation. (e.g. If you have a device that has a charging port at the bottom,
+ * rotating three times in counter clock direction will give you {@link Surface#ROTATION_270} while
+ * having the charging port on the left side of the device.)
+ *
+ * <p>If you need whether the device is in portrait or landscape (or their opposites), please add a
+ * function here that respects the device aspect ratio and
+ * {@code android.internal.R.bool.config_reverseDefaultRotation} together.
+ *
+ * <p>Note that {@code android.internal.R.bool.config_reverseDefaultRotation} does not change the
+ * winding order. In other words, the rotation order (counter clock-wise) will remain the same. It
+ * only flips the device orientation, such that portrait becomes upside down, landscape becomes
+ * seascape.
+ */
+public final class RotationUtils {
public static final int ROTATION_NONE = 0;
public static final int ROTATION_LANDSCAPE = 1;
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 1a30b0a..85fada2 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
@@ -20,14 +20,18 @@
import android.net.Uri;
import android.provider.Settings;
+import com.android.systemui.settings.UserTracker;
+
import javax.inject.Inject;
class GlobalSettingsImpl implements GlobalSettings {
private final ContentResolver mContentResolver;
+ private final UserTracker mUserTracker;
@Inject
- GlobalSettingsImpl(ContentResolver contentResolver) {
+ GlobalSettingsImpl(ContentResolver contentResolver, UserTracker userTracker) {
mContentResolver = contentResolver;
+ mUserTracker = userTracker;
}
@Override
@@ -36,13 +40,19 @@
}
@Override
+ public UserTracker getUserTracker() {
+ return mUserTracker;
+ }
+
+ @Override
public Uri getUriFor(String name) {
return Settings.Global.getUriFor(name);
}
@Override
public String getStringForUser(String name, int userHandle) {
- return Settings.Global.getStringForUser(mContentResolver, name, userHandle);
+ return Settings.Global.getStringForUser(mContentResolver, name,
+ getRealUserHandle(userHandle));
}
@Override
@@ -53,14 +63,16 @@
@Override
public boolean putStringForUser(String name, String value, int userHandle) {
- return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle);
+ return Settings.Global.putStringForUser(mContentResolver, name, value,
+ getRealUserHandle(userHandle));
}
@Override
public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
int userHandle, boolean overrideableByRestore) {
return Settings.Global.putStringForUser(
- mContentResolver, name, value, tag, makeDefault, userHandle, overrideableByRestore);
+ mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle),
+ overrideableByRestore);
}
@Override
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 020c234..f995436 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
@@ -20,14 +20,18 @@
import android.net.Uri;
import android.provider.Settings;
+import com.android.systemui.settings.UserTracker;
+
import javax.inject.Inject;
class SecureSettingsImpl implements SecureSettings {
private final ContentResolver mContentResolver;
+ private final UserTracker mUserTracker;
@Inject
- SecureSettingsImpl(ContentResolver contentResolver) {
+ SecureSettingsImpl(ContentResolver contentResolver, UserTracker userTracker) {
mContentResolver = contentResolver;
+ mUserTracker = userTracker;
}
@Override
@@ -36,13 +40,19 @@
}
@Override
+ public UserTracker getUserTracker() {
+ return mUserTracker;
+ }
+
+ @Override
public Uri getUriFor(String name) {
return Settings.Secure.getUriFor(name);
}
@Override
public String getStringForUser(String name, int userHandle) {
- return Settings.Secure.getStringForUser(mContentResolver, name, userHandle);
+ return Settings.Secure.getStringForUser(mContentResolver, name,
+ getRealUserHandle(userHandle));
}
@Override
@@ -52,14 +62,16 @@
@Override
public boolean putStringForUser(String name, String value, int userHandle) {
- return Settings.Secure.putStringForUser(mContentResolver, name, value, 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) {
return Settings.Secure.putStringForUser(
- mContentResolver, name, value, tag, makeDefault, userHandle, overrideableByRestore);
+ mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle),
+ overrideableByRestore);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
index 1bf5f07..b6846a3 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
@@ -22,8 +22,11 @@
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.UserHandle;
import android.provider.Settings;
+import com.android.systemui.settings.UserTracker;
+
/**
* Used to interact with Settings.Secure, Settings.Global, and Settings.System.
*
@@ -46,6 +49,11 @@
ContentResolver getContentResolver();
/**
+ * Returns that {@link UserTracker} this instance was constructed with.
+ */
+ UserTracker getUserTracker();
+
+ /**
* Returns the user id for the associated {@link ContentResolver}.
*/
default int getUserId() {
@@ -53,6 +61,17 @@
}
/**
+ * Returns the actual current user handle when querying with the current user. Otherwise,
+ * returns the passed in user id.
+ */
+ default int getRealUserHandle(int userHandle) {
+ if (userHandle != UserHandle.USER_CURRENT) {
+ return userHandle;
+ }
+ return getUserTracker().getUserId();
+ }
+
+ /**
* Construct the content URI for a particular name/value pair,
* useful for monitoring changes with a ContentObserver.
* @param name to look up in the table
@@ -84,18 +103,18 @@
*
* Implicitly calls {@link #getUriFor(String)} on the passed in name.
*/
- default void registerContentObserver(String name, boolean notifyForDescendents,
+ default void registerContentObserver(String name, boolean notifyForDescendants,
ContentObserver settingsObserver) {
- registerContentObserver(getUriFor(name), notifyForDescendents, settingsObserver);
+ registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
*/
- default void registerContentObserver(Uri uri, boolean notifyForDescendents,
+ default void registerContentObserver(Uri uri, boolean notifyForDescendants,
ContentObserver settingsObserver) {
- registerContentObserverForUser(uri, notifyForDescendents, settingsObserver, getUserId());
+ registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
}
/**
@@ -127,10 +146,10 @@
* Implicitly calls {@link #getUriFor(String)} on the passed in name.
*/
default void registerContentObserverForUser(
- String name, boolean notifyForDescendents, ContentObserver settingsObserver,
+ String name, boolean notifyForDescendants, ContentObserver settingsObserver,
int userHandle) {
registerContentObserverForUser(
- getUriFor(name), notifyForDescendents, settingsObserver, userHandle);
+ getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
}
/**
@@ -138,10 +157,10 @@
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
*/
default void registerContentObserverForUser(
- Uri uri, boolean notifyForDescendents, ContentObserver settingsObserver,
+ Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
int userHandle) {
getContentResolver().registerContentObserver(
- uri, notifyForDescendents, settingsObserver, userHandle);
+ uri, notifyForDescendants, settingsObserver, getRealUserHandle(userHandle));
}
/** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
index 0b8257d..561495e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
@@ -19,7 +19,6 @@
import android.annotation.UserIdInt
import android.database.ContentObserver
-import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -29,8 +28,8 @@
/** Returns a flow of [Unit] that is invoked each time that content is updated. */
fun SettingsProxy.observerFlow(
+ @UserIdInt userId: Int,
vararg names: String,
- @UserIdInt userId: Int = UserHandle.USER_CURRENT,
): Flow<Unit> {
return conflatedCallbackFlow {
val observer =
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 0dbb76f..fba7ddf 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
@@ -20,14 +20,18 @@
import android.net.Uri;
import android.provider.Settings;
+import com.android.systemui.settings.UserTracker;
+
import javax.inject.Inject;
class SystemSettingsImpl implements SystemSettings {
private final ContentResolver mContentResolver;
+ private final UserTracker mUserTracker;
@Inject
- SystemSettingsImpl(ContentResolver contentResolver) {
+ SystemSettingsImpl(ContentResolver contentResolver, UserTracker userTracker) {
mContentResolver = contentResolver;
+ mUserTracker = userTracker;
}
@Override
@@ -36,13 +40,19 @@
}
@Override
+ public UserTracker getUserTracker() {
+ return mUserTracker;
+ }
+
+ @Override
public Uri getUriFor(String name) {
return Settings.System.getUriFor(name);
}
@Override
public String getStringForUser(String name, int userHandle) {
- return Settings.System.getStringForUser(mContentResolver, name, userHandle);
+ return Settings.System.getStringForUser(mContentResolver, name,
+ getRealUserHandle(userHandle));
}
@Override
@@ -52,7 +62,8 @@
@Override
public boolean putStringForUser(String name, String value, int userHandle) {
- return Settings.System.putStringForUser(mContentResolver, name, value, userHandle);
+ return Settings.System.putStringForUser(mContentResolver, name, value,
+ getRealUserHandle(userHandle));
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
index f71d988..a453726 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
@@ -25,6 +25,7 @@
import android.provider.Settings;
import android.view.WindowManager.LayoutParams;
+import com.android.internal.R;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoMode;
@@ -55,7 +56,7 @@
public static final String VOLUME_UP_SILENT = "sysui_volume_up_silent";
public static final String VOLUME_SILENT_DO_NOT_DISTURB = "sysui_do_not_disturb";
- public static final boolean DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT = false;
+ private final boolean mDefaultVolumeDownToEnterSilent;
public static final boolean DEFAULT_VOLUME_UP_TO_EXIT_SILENT = false;
public static final boolean DEFAULT_DO_NOT_DISTURB_WHEN_SILENT = false;
@@ -72,12 +73,7 @@
private final KeyguardViewMediator mKeyguardViewMediator;
private final ActivityStarter mActivityStarter;
private VolumeDialog mDialog;
- private VolumePolicy mVolumePolicy = new VolumePolicy(
- DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT, // volumeDownToEnterSilent
- DEFAULT_VOLUME_UP_TO_EXIT_SILENT, // volumeUpToExitSilent
- DEFAULT_DO_NOT_DISTURB_WHEN_SILENT, // doNotDisturbWhenSilent
- 400 // vibrateToSilentDebounce
- );
+ private VolumePolicy mVolumePolicy;
@Inject
public VolumeDialogComponent(
@@ -107,7 +103,20 @@
mDialog = dialog;
mDialog.init(LayoutParams.TYPE_VOLUME_OVERLAY, mVolumeDialogCallback);
}).build();
+
+
+ mDefaultVolumeDownToEnterSilent = mContext.getResources()
+ .getBoolean(R.bool.config_volume_down_to_enter_silent);
+
+ mVolumePolicy = new VolumePolicy(
+ mDefaultVolumeDownToEnterSilent, // volumeDownToEnterSilent
+ DEFAULT_VOLUME_UP_TO_EXIT_SILENT, // volumeUpToExitSilent
+ DEFAULT_DO_NOT_DISTURB_WHEN_SILENT, // doNotDisturbWhenSilent
+ 400 // vibrateToSilentDebounce
+ );
+
applyConfiguration();
+
tunerService.addTunable(this, VOLUME_DOWN_SILENT, VOLUME_UP_SILENT,
VOLUME_SILENT_DO_NOT_DISTURB);
demoModeController.addCallback(this);
@@ -121,7 +130,7 @@
if (VOLUME_DOWN_SILENT.equals(key)) {
volumeDownToEnterSilent =
- TunerService.parseIntegerSwitch(newValue, DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT);
+ TunerService.parseIntegerSwitch(newValue, mDefaultVolumeDownToEnterSilent);
} else if (VOLUME_UP_SILENT.equals(key)) {
volumeUpToExitSilent =
TunerService.parseIntegerSwitch(newValue, DEFAULT_VOLUME_UP_TO_EXIT_SILENT);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 98d904e..89b66ee 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -46,7 +46,6 @@
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
-import android.os.UserHandle;
import android.os.VibrationEffect;
import android.provider.Settings;
import android.service.notification.Condition;
@@ -69,6 +68,7 @@
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.util.RingerModeLiveData;
import com.android.systemui.util.RingerModeTracker;
@@ -133,6 +133,7 @@
private final CaptioningManager mCaptioningManager;
private final KeyguardManager mKeyguardManager;
private final ActivityManager mActivityManager;
+ private final UserTracker mUserTracker;
protected C mCallbacks = new C();
private final State mState = new State();
protected final MediaSessionsCallbacks mMediaSessionsCallbacksW;
@@ -180,6 +181,7 @@
CaptioningManager captioningManager,
KeyguardManager keyguardManager,
ActivityManager activityManager,
+ UserTracker userTracker,
DumpManager dumpManager
) {
mContext = context.getApplicationContext();
@@ -209,6 +211,7 @@
mCaptioningManager = captioningManager;
mKeyguardManager = keyguardManager;
mActivityManager = activityManager;
+ mUserTracker = userTracker;
dumpManager.registerDumpable("VolumeDialogControllerImpl", this);
boolean accessibilityVolumeStreamActive = accessibilityManager
@@ -371,7 +374,7 @@
if (System.currentTimeMillis() - mLastToggledRingerOn < TOUCH_FEEDBACK_TIMEOUT_MS) {
try {
mAudioService.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD,
- UserHandle.USER_CURRENT);
+ mUserTracker.getUserId());
} catch (RemoteException e) {
// ignore
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index fa3c73a..9a37b0d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -1489,6 +1489,7 @@
.setDuration(mDialogHideAnimationDurationMs)
.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
.withEndAction(() -> mHandler.postDelayed(() -> {
+ mController.notifyVisible(false);
mDialog.dismiss();
tryToRemoveCaptionsTooltip();
mIsAnimatingDismiss = false;
@@ -1499,7 +1500,6 @@
animator.setListener(getJankListener(getDialogView(), TYPE_DISMISS,
mDialogHideAnimationDurationMs)).start();
checkODICaptionsTooltip(true);
- mController.notifyVisible(false);
synchronized (mSafetyWarningLock) {
if (mSafetyWarning != null) {
if (D.BUG) Log.d(TAG, "SafetyWarning dismissed");
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
index 4cbc709..4da5d49 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
@@ -273,7 +273,7 @@
};
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT),
+ Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT,
false /* notifyForDescendants */,
mDefaultPaymentAppObserver,
UserHandle.USER_ALL);
@@ -293,7 +293,7 @@
};
mSecureSettings.registerContentObserverForUser(
- Settings.Secure.getUriFor(QuickAccessWalletClientImpl.SETTING_KEY),
+ QuickAccessWalletClientImpl.SETTING_KEY,
false /* notifyForDescendants */,
mWalletPreferenceObserver,
UserHandle.USER_ALL);
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
new file mode 100644
index 0000000..7b8235a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.wallet.controller
+
+import android.Manifest
+import android.content.Context
+import android.content.IntentFilter
+import android.service.quickaccesswallet.GetWalletCardsError
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.service.quickaccesswallet.WalletCard
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.shareIn
+
+@SysUISingleton
+class WalletContextualSuggestionsController
+@Inject
+constructor(
+ @Application private val applicationCoroutineScope: CoroutineScope,
+ private val walletController: QuickAccessWalletController,
+ broadcastDispatcher: BroadcastDispatcher,
+ featureFlags: FeatureFlags
+) {
+ private val allWalletCards: Flow<List<WalletCard>> =
+ if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+ conflatedCallbackFlow {
+ val callback =
+ object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
+ override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) {
+ trySendWithFailureLogging(response.walletCards, TAG)
+ }
+
+ override fun onWalletCardRetrievalError(error: GetWalletCardsError) {
+ trySendWithFailureLogging(emptyList<WalletCard>(), TAG)
+ }
+ }
+
+ walletController.setupWalletChangeObservers(
+ callback,
+ QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+ QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+ )
+ walletController.updateWalletPreference()
+ walletController.queryWalletCards(callback)
+
+ awaitClose {
+ walletController.unregisterWalletChangeObservers(
+ QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+ QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+ )
+ }
+ }
+ } else {
+ emptyFlow()
+ }
+
+ private val contextualSuggestionsCardIds: Flow<Set<String>> =
+ if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+ broadcastDispatcher.broadcastFlow(
+ filter = IntentFilter(ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS),
+ permission = Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE,
+ flags = Context.RECEIVER_EXPORTED
+ ) { intent, _ ->
+ if (intent.hasExtra(UPDATE_CARD_IDS_EXTRA)) {
+ intent.getStringArrayListExtra(UPDATE_CARD_IDS_EXTRA).toSet()
+ } else {
+ emptySet()
+ }
+ }
+ } else {
+ emptyFlow()
+ }
+
+ val contextualSuggestionCards: Flow<List<WalletCard>> =
+ combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
+ cards.filter { card -> ids.contains(card.cardId) }
+ }
+ .shareIn(applicationCoroutineScope, replay = 1, started = SharingStarted.Eagerly)
+
+ companion object {
+ private const val ACTION_UPDATE_WALLET_CONTEXTUAL_SUGGESTIONS =
+ "com.android.systemui.wallet.UPDATE_CONTEXTUAL_SUGGESTIONS"
+
+ private const val UPDATE_CARD_IDS_EXTRA = "cardIds"
+
+ private const val TAG = "WalletSuggestions"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 1f1b32c..8b925b7 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -26,7 +26,6 @@
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Trace;
-import android.os.UserHandle;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.view.Surface;
@@ -37,6 +36,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.FileDescriptor;
@@ -58,6 +58,8 @@
private volatile int mPages = 1;
private boolean mPagesComputed = false;
+ private final UserTracker mUserTracker;
+
// used for most tasks (call canvas.drawBitmap, load/unload the bitmap)
@Background
private final DelayableExecutor mBackgroundExecutor;
@@ -66,9 +68,11 @@
private static final int DELAY_UNLOAD_BITMAP = 2000;
@Inject
- public ImageWallpaper(@Background DelayableExecutor backgroundExecutor) {
+ public ImageWallpaper(@Background DelayableExecutor backgroundExecutor,
+ UserTracker userTracker) {
super();
mBackgroundExecutor = backgroundExecutor;
+ mUserTracker = userTracker;
}
@Override
@@ -288,7 +292,7 @@
boolean loadSuccess = false;
Bitmap bitmap;
try {
- bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false);
+ bitmap = mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false);
if (bitmap != null
&& bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
throw new RuntimeException("Wallpaper is too large to draw!");
@@ -300,9 +304,9 @@
// default wallpaper can't be loaded.
Log.w(TAG, "Unable to load wallpaper!", exception);
mWallpaperManager.clearWallpaper(
- WallpaperManager.FLAG_SYSTEM, UserHandle.USER_CURRENT);
+ WallpaperManager.FLAG_SYSTEM, mUserTracker.getUserId());
try {
- bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false);
+ bitmap = mWallpaperManager.getBitmapAsUser(mUserTracker.getUserId(), false);
} catch (RuntimeException | OutOfMemoryError e) {
Log.w(TAG, "Unable to load default wallpaper!", e);
bitmap = null;
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 7033ccd..5d896cb 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -236,7 +236,8 @@
// Store callback in a field so it won't get GC'd
mStatusBarWindowCallback =
- (keyguardShowing, keyguardOccluded, bouncerShowing, isDozing, panelExpanded) ->
+ (keyguardShowing, keyguardOccluded, bouncerShowing, isDozing, panelExpanded,
+ isDreaming) ->
mBubbles.onNotificationPanelExpandedChanged(panelExpanded);
notificationShadeWindowController.registerCallback(mStatusBarWindowCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 8ef98f0..bd60401 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -16,8 +16,6 @@
package com.android.systemui.wmshell;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
@@ -50,6 +48,7 @@
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.notetask.NoteTaskInitializer;
+import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shared.tracing.ProtoTraceable;
import com.android.systemui.statusbar.CommandQueue;
@@ -124,6 +123,7 @@
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final ProtoTracer mProtoTracer;
private final UserTracker mUserTracker;
+ private final DisplayTracker mDisplayTracker;
private final NoteTaskInitializer mNoteTaskInitializer;
private final Executor mSysUiMainExecutor;
@@ -186,6 +186,7 @@
ProtoTracer protoTracer,
WakefulnessLifecycle wakefulnessLifecycle,
UserTracker userTracker,
+ DisplayTracker displayTracker,
NoteTaskInitializer noteTaskInitializer,
@Main Executor sysUiMainExecutor) {
mContext = context;
@@ -203,6 +204,7 @@
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
mUserTracker = userTracker;
+ mDisplayTracker = displayTracker;
mNoteTaskInitializer = noteTaskInitializer;
mSysUiMainExecutor = sysUiMainExecutor;
}
@@ -268,7 +270,7 @@
public void onStartTransition(boolean isEntering) {
mSysUiMainExecutor.execute(() -> {
mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
- true).commitUpdate(DEFAULT_DISPLAY);
+ true).commitUpdate(mDisplayTracker.getDefaultDisplayId());
});
}
@@ -276,7 +278,7 @@
public void onStartFinished(Rect bounds) {
mSysUiMainExecutor.execute(() -> {
mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
- true).commitUpdate(DEFAULT_DISPLAY);
+ true).commitUpdate(mDisplayTracker.getDefaultDisplayId());
});
}
@@ -284,7 +286,7 @@
public void onStopFinished(Rect bounds) {
mSysUiMainExecutor.execute(() -> {
mSysUiState.setFlag(SYSUI_STATE_ONE_HANDED_ACTIVE,
- false).commitUpdate(DEFAULT_DISPLAY);
+ false).commitUpdate(mDisplayTracker.getDefaultDisplayId());
});
}
});
@@ -333,7 +335,8 @@
@Override
public void setImeWindowStatus(int displayId, IBinder token, int vis,
int backDisposition, boolean showImeSwitcher) {
- if (displayId == DEFAULT_DISPLAY && (vis & InputMethodService.IME_VISIBLE) != 0) {
+ if (displayId == mDisplayTracker.getDefaultDisplayId()
+ && (vis & InputMethodService.IME_VISIBLE) != 0) {
oneHanded.stopOneHanded(
OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT);
}
@@ -346,7 +349,7 @@
@Override
public void onVisibilityChanged(boolean hasFreeformTasks) {
mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
- .commitUpdate(DEFAULT_DISPLAY);
+ .commitUpdate(mDisplayTracker.getDefaultDisplayId());
}
}, mSysUiMainExecutor);
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index e8f8e25..b9c23d4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -33,6 +33,7 @@
import com.android.systemui.plugins.ClockFaceController
import com.android.systemui.plugins.ClockFaceEvents
import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.any
@@ -40,8 +41,6 @@
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
-import java.util.TimeZone
-import java.util.concurrent.Executor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
@@ -50,15 +49,16 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
+import java.util.*
+import java.util.concurrent.Executor
+import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidTestingRunner::class)
@SmallTest
@@ -81,8 +81,10 @@
@Mock private lateinit var largeClockEvents: ClockFaceEvents
@Mock private lateinit var parentView: View
@Mock private lateinit var transitionRepository: KeyguardTransitionRepository
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var repository: FakeKeyguardRepository
- @Mock private lateinit var logBuffer: LogBuffer
+ @Mock private lateinit var smallLogBuffer: LogBuffer
+ @Mock private lateinit var largeLogBuffer: LogBuffer
private lateinit var underTest: ClockEventController
@Before
@@ -99,7 +101,7 @@
repository = FakeKeyguardRepository()
underTest = ClockEventController(
- KeyguardInteractor(repository = repository),
+ KeyguardInteractor(repository = repository, commandQueue = commandQueue),
KeyguardTransitionInteractor(repository = transitionRepository),
broadcastDispatcher,
batteryController,
@@ -109,7 +111,8 @@
context,
mainExecutor,
bgExecutor,
- logBuffer,
+ smallLogBuffer,
+ largeLogBuffer,
featureFlags
)
underTest.clock = clock
@@ -136,8 +139,9 @@
@Test
fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
- verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
- verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
+ // TODO(b/266103601): delete this test and add more coverage for updateColors()
+ // verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
+ // verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor))
@@ -148,9 +152,6 @@
@Test
fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
- verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
- verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
-
val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
verify(configurationController).addCallback(capture(captor))
captor.value.onDensityOrFontScaleChanged()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index 1059543..50645e5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -150,4 +150,11 @@
getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
false);
}
+
+
+ @Test
+ public void testReset() {
+ mKeyguardAbsKeyInputViewController.reset();
+ verify(mKeyguardMessageAreaController).setMessage("", false);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index c8e7538..512c351 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,11 +29,11 @@
import android.content.res.Resources;
import android.database.ContentObserver;
-import android.net.Uri;
import android.os.UserHandle;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
@@ -48,6 +48,7 @@
import com.android.systemui.plugins.ClockController;
import com.android.systemui.plugins.ClockEvents;
import com.android.systemui.plugins.ClockFaceController;
+import com.android.systemui.plugins.log.LogBuffer;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.AnimatableClockView;
import com.android.systemui.shared.clocks.ClockRegistry;
@@ -115,7 +116,14 @@
private FrameLayout mLargeClockFrame;
@Mock
private SecureSettings mSecureSettings;
+ @Mock
+ private LogBuffer mLogBuffer;
+ private final View mFakeDateView = (View) (new ViewGroup(mContext) {
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {}
+ });
+ private final View mFakeWeatherView = new View(mContext);
private final View mFakeSmartspaceView = new View(mContext);
private KeyguardClockSwitchController mController;
@@ -143,6 +151,8 @@
when(mLargeClockView.getContext()).thenReturn(getContext());
when(mView.isAttachedToWindow()).thenReturn(true);
+ when(mSmartspaceController.buildAndConnectDateView(any())).thenReturn(mFakeDateView);
+ when(mSmartspaceController.buildAndConnectWeatherView(any())).thenReturn(mFakeWeatherView);
when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
mExecutor = new FakeExecutor(new FakeSystemClock());
mController = new KeyguardClockSwitchController(
@@ -156,7 +166,8 @@
mSecureSettings,
mExecutor,
mDumpManager,
- mClockEventController
+ mClockEventController,
+ mLogBuffer
);
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
@@ -249,6 +260,19 @@
}
@Test
+ public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
+ when(mSmartspaceController.isEnabled()).thenReturn(true);
+ when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
+ mController.init();
+
+ mController.onLocaleListChanged();
+ // Should be called once on initial setup, then once again for locale change
+ verify(mSmartspaceController, times(2)).buildAndConnectDateView(mView);
+ verify(mSmartspaceController, times(2)).buildAndConnectWeatherView(mView);
+ verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
+ }
+
+ @Test
public void testSmartspaceDisabledShowsKeyguardStatusArea() {
when(mSmartspaceController.isEnabled()).thenReturn(false);
mController.init();
@@ -271,7 +295,7 @@
ArgumentCaptor<ContentObserver> observerCaptor =
ArgumentCaptor.forClass(ContentObserver.class);
mController.init();
- verify(mSecureSettings).registerContentObserverForUser(any(Uri.class),
+ verify(mSecureSettings).registerContentObserverForUser(any(String.class),
anyBoolean(), observerCaptor.capture(), eq(UserHandle.USER_ALL));
ContentObserver observer = observerCaptor.getValue();
mExecutor.runAllReady();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 254f953..8dc1e8f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -16,6 +16,7 @@
package com.android.keyguard;
+import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.android.keyguard.KeyguardClockSwitch.LARGE;
@@ -189,6 +190,7 @@
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
@@ -198,6 +200,7 @@
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
@@ -212,6 +215,7 @@
// only big clock is removed at switch
assertThat(mLargeClockFrame.getParent()).isNull();
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
@@ -223,6 +227,7 @@
// only big clock is removed at switch
assertThat(mLargeClockFrame.getParent()).isNull();
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
index 01365b4..1a365ef 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
@@ -25,9 +25,7 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -39,6 +37,7 @@
import com.android.keyguard.dagger.KeyguardStatusViewComponent;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.settings.FakeDisplayTracker;
import org.junit.Before;
import org.junit.Test;
@@ -58,12 +57,12 @@
@Mock
private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
@Mock
- private DisplayManager mDisplayManager;
- @Mock
private KeyguardDisplayManager.KeyguardPresentation mKeyguardPresentation;
+ private Executor mMainExecutor = Runnable::run;
private Executor mBackgroundExecutor = Runnable::run;
private KeyguardDisplayManager mManager;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
// The default and secondary displays are both in the default group
private Display mDefaultDisplay;
@@ -75,9 +74,9 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController,
- mKeyguardStatusViewComponentFactory, mBackgroundExecutor));
+ mKeyguardStatusViewComponentFactory, mDisplayTracker, mMainExecutor,
+ mBackgroundExecutor));
doReturn(mKeyguardPresentation).when(mManager).createPresentation(any());
mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY,
@@ -96,23 +95,21 @@
@Test
public void testShow_defaultDisplayOnly() {
- when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay});
mManager.show();
verify(mManager, never()).createPresentation(any());
}
@Test
public void testShow_includeSecondaryDisplay() {
- when(mDisplayManager.getDisplays()).thenReturn(
- new Display[]{mDefaultDisplay, mSecondaryDisplay});
+ mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
mManager.show();
verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
}
@Test
public void testShow_includeAlwaysUnlockedDisplay() {
- when(mDisplayManager.getDisplays()).thenReturn(
- new Display[]{mDefaultDisplay, mAlwaysUnlockedDisplay});
+ mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mAlwaysUnlockedDisplay});
mManager.show();
verify(mManager, never()).createPresentation(any());
@@ -120,9 +117,8 @@
@Test
public void testShow_includeSecondaryAndAlwaysUnlockedDisplays() {
- when(mDisplayManager.getDisplays()).thenReturn(
+ mDisplayTracker.setAllDisplays(
new Display[]{mDefaultDisplay, mSecondaryDisplay, mAlwaysUnlockedDisplay});
-
mManager.show();
verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 84f6d91..075ef9d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -405,6 +405,13 @@
}
@Test
+ public void onBouncerVisibilityChanged_resetsScale() {
+ mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.INVISIBLE);
+
+ verify(mView).resetScale();
+ }
+
+ @Test
public void onStartingToHide_sideFpsHintShown_sideFpsHintHidden() {
setupGetSecurityView();
setupConditionsToEnableSideFpsHint();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 36ed669..1bbc199 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -49,6 +49,8 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.test.filters.SmallTest;
@@ -357,6 +359,27 @@
assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(PARENT_ID);
}
+ @Test
+ public void testPlayBackAnimation() {
+ OnBackAnimationCallback backCallback = mKeyguardSecurityContainer.getBackCallback();
+ backCallback.onBackStarted(createBackEvent(0, 0));
+ mKeyguardSecurityContainer.getBackCallback().onBackProgressed(
+ createBackEvent(0, 1));
+ assertThat(mKeyguardSecurityContainer.getScaleX()).isEqualTo(
+ KeyguardSecurityContainer.MIN_BACK_SCALE);
+ assertThat(mKeyguardSecurityContainer.getScaleY()).isEqualTo(
+ KeyguardSecurityContainer.MIN_BACK_SCALE);
+
+ // reset scale
+ mKeyguardSecurityContainer.resetScale();
+ assertThat(mKeyguardSecurityContainer.getScaleX()).isEqualTo(1);
+ assertThat(mKeyguardSecurityContainer.getScaleY()).isEqualTo(1);
+ }
+
+ private BackEvent createBackEvent(float touchX, float progress) {
+ return new BackEvent(0, 0, progress, BackEvent.EDGE_LEFT);
+ }
+
private Configuration configuration(@Configuration.Orientation int orientation) {
Configuration config = new Configuration();
config.orientation = orientation;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
index 06082b6..68dc6c0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
@@ -29,6 +29,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
@@ -52,6 +53,7 @@
private ConfigurationController mConfigurationController;
@Mock
private ActivityStarter mActivityStarter;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private DumpManager mDumpManager = new DumpManager();
private KeyguardSliceViewController mController;
@@ -63,7 +65,7 @@
when(mView.getContext()).thenReturn(mContext);
mController = new KeyguardSliceViewController(
mView, mActivityStarter, mConfigurationController,
- mTunerService, mDumpManager);
+ mTunerService, mDumpManager, mDisplayTracker);
mController.setupUri(KeyguardSliceProvider.KEYGUARD_SLICE_URI);
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index be4bbdf..dfad15d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -24,6 +24,7 @@
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
+import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ClockAnimations;
@@ -65,6 +66,8 @@
ScreenOffAnimationController mScreenOffAnimationController;
@Captor
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
+ @Mock
+ KeyguardLogger mKeyguardLogger;
private KeyguardStatusViewController mController;
@@ -81,7 +84,8 @@
mConfigurationController,
mDozeParameters,
mFeatureFlags,
- mScreenOffAnimationController);
+ mScreenOffAnimationController,
+ mKeyguardLogger);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 13cd328..d1650b7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -17,7 +17,6 @@
package com.android.keyguard;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
@@ -32,13 +31,15 @@
import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT;
import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED;
+import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyObject;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -116,6 +117,7 @@
import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.log.SessionTracker;
@@ -123,6 +125,7 @@
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.telephony.TelephonyListenerManager;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings;
@@ -142,6 +145,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -191,6 +195,8 @@
@Mock
private DevicePolicyManager mDevicePolicyManager;
@Mock
+ private DevicePostureController mDevicePostureController;
+ @Mock
private IDreamManager mDreamManager;
@Mock
private KeyguardBypassController mKeyguardBypassController;
@@ -229,10 +235,10 @@
@Mock
private UiEventLogger mUiEventLogger;
@Mock
- private PowerManager mPowerManager;
- @Mock
private GlobalSettings mGlobalSettings;
private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
+ @Mock
+ private FingerprintInteractiveToAuthProvider mInteractiveToAuthProvider;
private final int mCurrentUserId = 100;
@@ -296,6 +302,7 @@
.thenReturn(new ServiceState());
when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings);
when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+ when(mDevicePostureController.getDevicePosture()).thenReturn(DEVICE_POSTURE_UNKNOWN);
mMockitoSession = ExtendedMockito.mockitoSession()
.spyStatic(SubscriptionManager.class)
@@ -307,6 +314,9 @@
when(mUserTracker.getUserId()).thenReturn(mCurrentUserId);
ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService);
+ mContext.getOrCreateTestableResources().addOverride(
+ com.android.systemui.R.integer.config_face_auth_supported_posture,
+ DEVICE_POSTURE_UNKNOWN);
mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig(
mContext.getResources(),
mGlobalSettings,
@@ -819,6 +829,19 @@
}
@Test
+ public void doesNotTryToAuthenticateWhenKeyguardIsNotShowingButOccluded_whenAssistant() {
+ mKeyguardUpdateMonitor.setKeyguardShowing(false, true);
+ mKeyguardUpdateMonitor.setAssistantVisible(true);
+
+ verify(mFaceManager, never()).authenticate(any(),
+ any(),
+ any(),
+ any(),
+ anyInt(),
+ anyBoolean());
+ }
+
+ @Test
public void testTriesToAuthenticate_whenTrustOnAgentKeyguard_ifBypass() {
mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
@@ -831,6 +854,32 @@
}
@Test
+ public void faceUnlockDoesNotRunWhenDeviceIsGoingToSleepWithAssistantVisible() {
+ mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
+ mKeyguardUpdateMonitor.setAssistantVisible(true);
+
+ verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+ mTestableLooper.processAllMessages();
+ clearInvocations(mFaceManager);
+
+ // Device going to sleep while assistant is visible
+ mKeyguardUpdateMonitor.handleStartedGoingToSleep(0);
+ mKeyguardUpdateMonitor.handleFinishedGoingToSleep(0);
+ mTestableLooper.moveTimeForward(DEFAULT_CANCEL_SIGNAL_TIMEOUT);
+ mTestableLooper.processAllMessages();
+
+ mKeyguardUpdateMonitor.handleKeyguardReset();
+
+ assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isFalse();
+ verify(mFaceManager, never()).authenticate(any(),
+ any(),
+ any(),
+ any(),
+ anyInt(),
+ anyBoolean());
+ }
+
+ @Test
public void testIgnoresAuth_whenTrustAgentOnKeyguard_withoutBypass() {
mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
@@ -1250,7 +1299,7 @@
}
@Test
- public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled()
+ public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled()
throws RemoteException {
// SFPS supported and enrolled
final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
@@ -1258,12 +1307,8 @@
when(mAuthController.getSfpsProps()).thenReturn(props);
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
- // WHEN require screen on to auth is disabled, and keyguard is not awake
- when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0);
- mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
-
- mContext.getOrCreateTestableResources().addOverride(
- com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true);
+ // WHEN require interactive to auth is disabled, and keyguard is not awake
+ when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false);
// Preconditions for sfps auth to run
keyguardNotGoingAway();
@@ -1279,9 +1324,8 @@
// THEN we should listen for sfps when screen off, because require screen on is disabled
assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
- // WHEN require screen on to auth is enabled, and keyguard is not awake
- when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1);
- mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
+ // WHEN require interactive to auth is enabled, and keyguard is not awake
+ when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true);
// THEN we shouldn't listen for sfps when screen off, because require screen on is enabled
assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
@@ -1295,6 +1339,61 @@
assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
}
+ @Test
+ public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled()
+ throws RemoteException {
+ // GIVEN SFPS supported and enrolled
+ final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+ props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON));
+ when(mAuthController.getSfpsProps()).thenReturn(props);
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+
+ // GIVEN Preconditions for sfps auth to run
+ keyguardNotGoingAway();
+ currentUserIsPrimary();
+ currentUserDoesNotHaveTrust();
+ biometricsNotDisabledThroughDevicePolicyManager();
+ biometricsEnabledForCurrentUser();
+ userNotCurrentlySwitching();
+ statusBarShadeIsLocked();
+
+ // WHEN require interactive to auth is enabled & keyguard is going to sleep
+ when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true);
+ deviceGoingToSleep();
+
+ mTestableLooper.processAllMessages();
+
+ // THEN we should NOT listen for sfps because device is going to sleep
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
+ }
+
+ @Test
+ public void listeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthDisabled()
+ throws RemoteException {
+ // GIVEN SFPS supported and enrolled
+ final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+ props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON));
+ when(mAuthController.getSfpsProps()).thenReturn(props);
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+
+ // GIVEN Preconditions for sfps auth to run
+ keyguardNotGoingAway();
+ currentUserIsPrimary();
+ currentUserDoesNotHaveTrust();
+ biometricsNotDisabledThroughDevicePolicyManager();
+ biometricsEnabledForCurrentUser();
+ userNotCurrentlySwitching();
+ statusBarShadeIsLocked();
+
+ // WHEN require interactive to auth is disabled & keyguard is going to sleep
+ when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false);
+ deviceGoingToSleep();
+
+ mTestableLooper.processAllMessages();
+
+ // THEN we should listen for sfps because screen on to auth is disabled
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+ }
private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal(
@FingerprintSensorProperties.SensorType int sensorType) {
@@ -1807,28 +1906,6 @@
}
@Test
- public void testFingerAcquired_wakesUpPowerManager() {
- cleanupKeyguardUpdateMonitor();
- mContext.getOrCreateTestableResources().addOverride(
- com.android.internal.R.bool.kg_wake_on_acquire_start, true);
- mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
- fingerprintAcquireStart();
-
- verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
- }
-
- @Test
- public void testFingerAcquired_doesNotWakeUpPowerManager() {
- cleanupKeyguardUpdateMonitor();
- mContext.getOrCreateTestableResources().addOverride(
- com.android.internal.R.bool.kg_wake_on_acquire_start, false);
- mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
- fingerprintAcquireStart();
-
- verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
- }
-
- @Test
public void testDreamingStopped_faceDoesNotRun() {
mKeyguardUpdateMonitor.dispatchDreamingStopped();
mTestableLooper.processAllMessages();
@@ -2187,6 +2264,54 @@
eq(true));
}
+ @Test
+ public void testShouldListenForFace_withAuthSupportPostureConfig_returnsTrue()
+ throws RemoteException {
+ mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED;
+ keyguardNotGoingAway();
+ bouncerFullyVisibleAndNotGoingToSleep();
+ currentUserIsPrimary();
+ currentUserDoesNotHaveTrust();
+ biometricsNotDisabledThroughDevicePolicyManager();
+ biometricsEnabledForCurrentUser();
+ userNotCurrentlySwitching();
+ supportsFaceDetection();
+
+ deviceInPostureStateOpened();
+ mTestableLooper.processAllMessages();
+ // Should not listen for face when posture state in DEVICE_POSTURE_OPENED
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
+
+ deviceInPostureStateClosed();
+ mTestableLooper.processAllMessages();
+ // Should listen for face when posture state in DEVICE_POSTURE_CLOSED
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+ }
+
+ @Test
+ public void testShouldListenForFace_withoutAuthSupportPostureConfig_returnsTrue()
+ throws RemoteException {
+ mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_UNKNOWN;
+ keyguardNotGoingAway();
+ bouncerFullyVisibleAndNotGoingToSleep();
+ currentUserIsPrimary();
+ currentUserDoesNotHaveTrust();
+ biometricsNotDisabledThroughDevicePolicyManager();
+ biometricsEnabledForCurrentUser();
+ userNotCurrentlySwitching();
+ supportsFaceDetection();
+
+ deviceInPostureStateClosed();
+ mTestableLooper.processAllMessages();
+ // Whether device in any posture state, always listen for face
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+
+ deviceInPostureStateOpened();
+ mTestableLooper.processAllMessages();
+ // Whether device in any posture state, always listen for face
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+ }
+
private void userDeviceLockDown() {
when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId))
@@ -2261,9 +2386,12 @@
.onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT, "Fingerprint locked out");
}
- private void fingerprintAcquireStart() {
- mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
- .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START);
+ private void deviceInPostureStateOpened() {
+ mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_OPENED);
+ }
+
+ private void deviceInPostureStateClosed() {
+ mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_CLOSED);
}
private void successfulFingerprintAuth() {
@@ -2404,10 +2532,11 @@
mAuthController, mTelephonyListenerManager,
mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
- mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
+ mTrustManager, mSubscriptionManager, mUserManager,
mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager,
- mFaceWakeUpTriggersConfig);
+ mFaceWakeUpTriggersConfig, mDevicePostureController,
+ Optional.of(mInteractiveToAuthProvider));
setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index e4c41a7..05bd1e4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -49,6 +49,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -91,6 +92,7 @@
protected @Mock AuthRippleController mAuthRippleController;
protected @Mock FeatureFlags mFeatureFlags;
protected @Mock KeyguardTransitionRepository mTransitionRepository;
+ protected @Mock CommandQueue mCommandQueue;
protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
protected LockIconViewController mUnderTest;
@@ -157,7 +159,7 @@
mAuthRippleController,
mResources,
new KeyguardTransitionInteractor(mTransitionRepository),
- new KeyguardInteractor(new FakeKeyguardRepository()),
+ new KeyguardInteractor(new FakeKeyguardRepository(), mCommandQueue),
mFeatureFlags
);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
index 81d0034..32edf8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -148,7 +148,7 @@
// Act
whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
- flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+ flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
// Assert
verify(mockPackageManager, times(2)).setComponentEnabledSetting(
@@ -175,7 +175,7 @@
// Act
whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
- flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+ flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
// Assert
verify(mockPackageManager, times(2)).setComponentEnabledSetting(
@@ -198,13 +198,13 @@
// Act
whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
- flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id + 1))
+ flagListener.value.onFlagChanged(TestFlagEvent("other flag"))
// Assert
verifyZeroInteractions(mockPackageManager)
}
- private class TestFlagEvent(override val flagId: Int) : FlagListenable.FlagEvent {
+ private class TestFlagEvent(override val flagName: String) : FlagListenable.FlagEvent {
override fun requestNoRestart() {}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 0627fc6..fc11148 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -21,6 +21,7 @@
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.systemui.dump.LogBufferHelperKt.logcatLogBuffer;
import static com.google.common.truth.Truth.assertThat;
@@ -88,7 +89,9 @@
import com.android.systemui.decor.PrivacyDotCornerDecorProviderImpl;
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
+import com.android.systemui.log.ScreenDecorationsLogger;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.tuner.TunerService;
@@ -117,6 +120,7 @@
private DisplayManager mDisplayManager;
private SecureSettings mSecureSettings;
private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+ private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private FakeThreadFactory mThreadFactory;
private ArrayList<DecorProvider> mPrivacyDecorProviders;
private ArrayList<DecorProvider> mFaceScanningProviders;
@@ -217,11 +221,14 @@
mAuthController,
mStatusBarStateController,
mKeyguardUpdateMonitor,
- mExecutor));
+ mExecutor,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer"))));
mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
- mTunerService, mUserTracker, mDotViewController, mThreadFactory,
- mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) {
+ mTunerService, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory,
+ mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+ new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")),
+ mAuthController) {
@Override
public void start() {
super.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
index 7aa4763..4a5c1be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
@@ -22,14 +22,16 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
-import android.os.UserHandle;
+import android.app.ActivityManager;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import org.junit.Before;
import org.junit.Rule;
@@ -42,11 +44,14 @@
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class AccessibilityButtonModeObserverTest extends SysuiTestCase {
+ private static final int MY_USER_ID = ActivityManager.getCurrentUser();
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
@Mock
+ private UserTracker mUserTracker;
+ @Mock
private AccessibilityButtonModeObserver.ModeChangedListener mListener;
private AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
@@ -56,10 +61,12 @@
@Before
public void setUp() {
+ when(mUserTracker.getUserId()).thenReturn(MY_USER_ID);
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
- Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
- mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext);
+ Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, MY_USER_ID);
+ mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext,
+ mUserTracker);
}
@Test
@@ -67,7 +74,7 @@
mAccessibilityButtonModeObserver.addListener(mListener);
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, TEST_A11Y_BTN_MODE_VALUE,
- UserHandle.USER_CURRENT);
+ MY_USER_ID);
mAccessibilityButtonModeObserver.mContentObserver.onChange(false);
@@ -80,7 +87,7 @@
mAccessibilityButtonModeObserver.removeListener(mListener);
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, TEST_A11Y_BTN_MODE_VALUE,
- UserHandle.USER_CURRENT);
+ MY_USER_ID);
mAccessibilityButtonModeObserver.mContentObserver.onChange(false);
@@ -91,7 +98,7 @@
public void getCurrentAccessibilityButtonMode_expectedValue() {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, TEST_A11Y_BTN_MODE_VALUE,
- UserHandle.USER_CURRENT);
+ MY_USER_ID);
final int actualValue =
mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
index 4145437..a5a7a4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
@@ -21,14 +21,16 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
-import android.os.UserHandle;
+import android.app.ActivityManager;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import org.junit.Before;
import org.junit.Rule;
@@ -42,11 +44,14 @@
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class AccessibilityButtonTargetsObserverTest extends SysuiTestCase {
+ private static final int MY_USER_ID = ActivityManager.getCurrentUser();
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
@Mock
+ private UserTracker mUserTracker;
+ @Mock
private AccessibilityButtonTargetsObserver.TargetsChangedListener mListener;
private AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
@@ -55,7 +60,9 @@
@Before
public void setUp() {
- mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext);
+ when(mUserTracker.getUserId()).thenReturn(MY_USER_ID);
+ mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext,
+ mUserTracker);
}
@Test
@@ -63,7 +70,7 @@
mAccessibilityButtonTargetsObserver.addListener(mListener);
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
- UserHandle.USER_CURRENT);
+ MY_USER_ID);
mAccessibilityButtonTargetsObserver.mContentObserver.onChange(false);
@@ -76,7 +83,7 @@
mAccessibilityButtonTargetsObserver.removeListener(mListener);
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
- UserHandle.USER_CURRENT);
+ MY_USER_ID);
mAccessibilityButtonTargetsObserver.mContentObserver.onChange(false);
@@ -87,7 +94,7 @@
public void getCurrentAccessibilityButtonTargets_expectedValue() {
Settings.Secure.putStringForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
- UserHandle.USER_CURRENT);
+ MY_USER_ID);
final String actualValue =
mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
index 58b4af4..da419d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
@@ -39,6 +39,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.model.SysUiState;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import org.junit.Before;
@@ -76,6 +77,7 @@
private IWindowMagnificationConnection mIWindowMagnificationConnection;
private WindowMagnification mWindowMagnification;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
@Before
public void setUp() throws Exception {
@@ -88,7 +90,7 @@
any(IWindowMagnificationConnection.class));
mWindowMagnification = new WindowMagnification(getContext(),
getContext().getMainThreadHandler(), mCommandQueue,
- mModeSwitchesController, mSysUiState, mOverviewProxyService);
+ mModeSwitchesController, mSysUiState, mOverviewProxyService, mDisplayTracker);
mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
mContext.getSystemService(DisplayManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
index 41fd2b3..9c601a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
@@ -18,18 +18,20 @@
import static com.google.common.truth.Truth.assertThat;
+import android.app.ActivityManager;
import android.content.Context;
-import android.os.UserHandle;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
/** Test for {@link SecureSettingsContentObserver}. */
@RunWith(AndroidTestingRunner.class)
@@ -40,7 +42,9 @@
@Before
public void setUpObserver() {
- mTestObserver = new FakeSecureSettingsContentObserver(mContext,
+ UserTracker userTracker = Mockito.mock(UserTracker.class);
+ Mockito.when(userTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+ mTestObserver = new FakeSecureSettingsContentObserver(mContext, userTracker,
Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
}
@@ -57,7 +61,7 @@
@Test
public void checkValue() {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 1, UserHandle.USER_CURRENT);
+ Settings.Secure.ACCESSIBILITY_BUTTON_MODE, 1, ActivityManager.getCurrentUser());
assertThat(mTestObserver.getSettingsValue()).isEqualTo("1");
}
@@ -66,9 +70,9 @@
private static class FakeSecureSettingsContentObserver extends
SecureSettingsContentObserver<Object> {
- protected FakeSecureSettingsContentObserver(Context context,
+ protected FakeSecureSettingsContentObserver(Context context, UserTracker userTracker,
String secureSettingsKey) {
- super(context, secureSettingsKey);
+ super(context, userTracker, secureSettingsKey);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index b7d3459..f4505f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -32,8 +32,11 @@
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.AdditionalAnswers.returnsSecondArg;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
@@ -75,7 +78,9 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.util.leak.ReferenceTestUtils;
+import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.utils.os.FakeHandler;
import org.junit.After;
@@ -111,14 +116,17 @@
IRemoteMagnificationAnimationCallback mAnimationCallback;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+ @Mock
+ private SecureSettings mSecureSettings;
private Handler mHandler;
private TestableWindowManager mWindowManager;
- private SysUiState mSysUiState = new SysUiState();
+ private SysUiState mSysUiState;
private Resources mResources;
private WindowMagnificationAnimationController mWindowMagnificationAnimationController;
private WindowMagnificationController mWindowMagnificationController;
private Instrumentation mInstrumentation;
+ private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0);
@Before
@@ -137,7 +145,12 @@
return null;
}).when(mSfVsyncFrameProvider).postFrameCallback(
any(FrameCallback.class));
+ mSysUiState = new SysUiState(mDisplayTracker);
mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class));
+ when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then(
+ returnsSecondArg());
+ when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then(
+ returnsSecondArg());
mResources = getContext().getOrCreateTestableResources().getResources();
mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
index ccf2f8b..f75dc03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
@@ -45,6 +45,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.model.SysUiState;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import org.junit.Before;
@@ -74,6 +75,8 @@
private CommandQueue mCommandQueue;
private WindowMagnification mWindowMagnification;
private OverviewProxyListener mOverviewProxyListener;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -87,10 +90,10 @@
when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
- mCommandQueue = new CommandQueue(getContext());
+ mCommandQueue = new CommandQueue(getContext(), new FakeDisplayTracker(getContext()));
mWindowMagnification = new WindowMagnification(getContext(),
getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController,
- mSysUiState, mOverviewProxyService);
+ mSysUiState, mOverviewProxyService, mDisplayTracker);
mWindowMagnification.start();
final ArgumentCaptor<OverviewProxyListener> listenerArgumentCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 8ca17b9..f34a36f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -25,6 +25,7 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import android.app.ActivityManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.UserHandle;
@@ -40,6 +41,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.After;
import org.junit.Before;
@@ -48,6 +50,8 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -71,8 +75,12 @@
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardCallbackCaptor;
private KeyguardUpdateMonitorCallback mKeyguardCallback;
+ @Mock
+ private SecureSettings mSecureSettings;
+
@Before
public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
mContextWrapper = new ContextWrapper(mContext) {
@Override
public Context createContextAsUser(UserHandle user, int flags) {
@@ -128,7 +136,7 @@
public void onKeyguardVisibilityChanged_showing_destroyWidget() {
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
- mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper);
+ mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserUnlocked();
@@ -154,7 +162,7 @@
final int fakeUserId = 1;
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
- mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper);
+ mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserSwitching(fakeUserId);
@@ -167,7 +175,7 @@
final int fakeUserId = 1;
enableAccessibilityFloatingMenuConfig();
mController = setUpController();
- mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper);
+ mController.mFloatingMenu = new AccessibilityFloatingMenu(mContextWrapper, mSecureSettings);
captureKeyguardUpdateMonitorCallback();
mKeyguardCallback.onUserUnlocked();
mKeyguardCallback.onKeyguardVisibilityChanged(true);
@@ -197,7 +205,7 @@
public void onAccessibilityButtonModeChanged_floatingModeAndHasButtonTargets_showWidget() {
Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
- UserHandle.USER_CURRENT);
+ ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
@@ -208,7 +216,7 @@
@Test
public void onAccessibilityButtonModeChanged_floatingModeAndNoButtonTargets_destroyWidget() {
Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU);
@@ -220,7 +228,7 @@
public void onAccessibilityButtonModeChanged_navBarModeAndHasButtonTargets_destroyWidget() {
Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
- UserHandle.USER_CURRENT);
+ ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
@@ -231,7 +239,7 @@
@Test
public void onAccessibilityButtonModeChanged_navBarModeAndNoButtonTargets_destroyWidget() {
Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR);
@@ -243,7 +251,7 @@
public void onAccessibilityButtonTargetsChanged_floatingModeAndHasButtonTargets_showWidget() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
- UserHandle.USER_CURRENT);
+ ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -255,7 +263,7 @@
public void onAccessibilityButtonTargetsChanged_floatingModeAndNoButtonTargets_destroyWidget() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
- UserHandle.USER_CURRENT);
+ ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonTargetsChanged("");
@@ -267,7 +275,7 @@
public void onAccessibilityButtonTargetsChanged_navBarModeAndHasButtonTargets_destroyWidget() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
- ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
+ ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS);
@@ -279,7 +287,7 @@
public void onAccessibilityButtonTargetsChanged_navBarModeAndNoButtonTargets_destroyWidget() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
- ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, UserHandle.USER_CURRENT);
+ ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser());
mController = setUpController();
mController.onAccessibilityButtonTargetsChanged("");
@@ -293,7 +301,7 @@
mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
final AccessibilityFloatingMenuController controller =
new AccessibilityFloatingMenuController(mContextWrapper, mTargetsObserver,
- mModeObserver, mKeyguardUpdateMonitor);
+ mModeObserver, mKeyguardUpdateMonitor, mSecureSettings);
controller.init();
return controller;
@@ -302,10 +310,10 @@
private void enableAccessibilityFloatingMenuConfig() {
Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU,
- UserHandle.USER_CURRENT);
+ ActivityManager.getCurrentUser());
Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS,
- UserHandle.USER_CURRENT);
+ ActivityManager.getCurrentUser());
}
private void captureKeyguardUpdateMonitorCallback() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
index 558261b..04345fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
@@ -31,6 +31,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.After;
import org.junit.Before;
@@ -55,6 +56,8 @@
@Mock
private AccessibilityManager mAccessibilityManager;
+ @Mock
+ private SecureSettings mSecureSettings;
private AccessibilityFloatingMenuView mMenuView;
private AccessibilityFloatingMenu mMenu;
@@ -69,7 +72,7 @@
final Position position = new Position(0, 0);
mMenuView = new AccessibilityFloatingMenuView(mContext, position);
- mMenu = new AccessibilityFloatingMenu(mContext, mMenuView);
+ mMenu = new AccessibilityFloatingMenu(mContext, mSecureSettings, mMenuView);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index a61cd23..578e1d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -15,6 +15,7 @@
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -26,6 +27,7 @@
import junit.framework.AssertionFailedError
import kotlin.concurrent.thread
import org.junit.After
+import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -195,6 +197,13 @@
verify(controller).onLaunchAnimationStart(anyBoolean())
}
+ @Test
+ fun creatingControllerFromNormalViewThrows() {
+ assertThrows(IllegalArgumentException::class.java) {
+ ActivityLaunchAnimator.Controller.fromView(FrameLayout(mContext))
+ }
+ }
+
private fun fakeWindow(): RemoteAnimationTarget {
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
val taskInfo = ActivityManager.RunningTaskInfo()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index 7c1e384..1e62fd23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -12,11 +12,13 @@
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.WindowManager
+import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.DecorView
import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull
@@ -24,6 +26,7 @@
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -205,25 +208,81 @@
verify(interactionJankMonitor).end(InteractionJankMonitor.CUJ_USER_DIALOG_OPEN)
}
+ @Test
+ fun testAnimationDoesNotChangeLaunchableViewVisibility_viewVisible() {
+ val touchSurface = createTouchSurface()
+
+ // View is VISIBLE when starting the animation.
+ runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.VISIBLE }
+
+ // View is invisible while the dialog is shown.
+ val dialog = showDialogFromView(touchSurface)
+ assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE)
+
+ // View is visible again when the dialog is dismissed.
+ runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
+ assertThat(touchSurface.visibility).isEqualTo(View.VISIBLE)
+ }
+
+ @Test
+ fun testAnimationDoesNotChangeLaunchableViewVisibility_viewInvisible() {
+ val touchSurface = createTouchSurface()
+
+ // View is INVISIBLE when starting the animation.
+ runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.INVISIBLE }
+
+ // View is INVISIBLE while the dialog is shown.
+ val dialog = showDialogFromView(touchSurface)
+ assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE)
+
+ // View is invisible like it was before showing the dialog.
+ runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
+ assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE)
+ }
+
+ @Test
+ fun testAnimationDoesNotChangeLaunchableViewVisibility_viewVisibleThenGone() {
+ val touchSurface = createTouchSurface()
+
+ // View is VISIBLE when starting the animation.
+ runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.VISIBLE }
+
+ // View is INVISIBLE while the dialog is shown.
+ val dialog = showDialogFromView(touchSurface)
+ assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE)
+
+ // Some external call makes the View GONE. It remains INVISIBLE while the dialog is shown,
+ // as all visibility changes should be blocked.
+ runOnMainThreadAndWaitForIdleSync { touchSurface.visibility = View.GONE }
+ assertThat(touchSurface.visibility).isEqualTo(View.INVISIBLE)
+
+ // View is restored to GONE once the dialog is dismissed.
+ runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
+ assertThat(touchSurface.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun creatingControllerFromNormalViewThrows() {
+ assertThrows(IllegalArgumentException::class.java) {
+ DialogLaunchAnimator.Controller.fromView(FrameLayout(mContext))
+ }
+ }
+
private fun createAndShowDialog(
animator: DialogLaunchAnimator = dialogLaunchAnimator,
): TestDialog {
val touchSurface = createTouchSurface()
- return runOnMainThreadAndWaitForIdleSync {
- val dialog = TestDialog(context)
- animator.showFromView(dialog, touchSurface)
- dialog
- }
+ return showDialogFromView(touchSurface, animator)
}
private fun createTouchSurface(): View {
return runOnMainThreadAndWaitForIdleSync {
val touchSurfaceRoot = LinearLayout(context)
- val touchSurface = View(context)
+ val touchSurface = TouchSurfaceView(context)
touchSurfaceRoot.addView(touchSurface)
// We need to attach the root to the window manager otherwise the exit animation will
- // be skipped
+ // be skipped.
ViewUtils.attachView(touchSurfaceRoot)
attachedViews.add(touchSurfaceRoot)
@@ -231,6 +290,17 @@
}
}
+ private fun showDialogFromView(
+ touchSurface: View,
+ animator: DialogLaunchAnimator = dialogLaunchAnimator,
+ ): TestDialog {
+ return runOnMainThreadAndWaitForIdleSync {
+ val dialog = TestDialog(context)
+ animator.showFromView(dialog, touchSurface)
+ dialog
+ }
+ }
+
private fun createDialogAndShowFromDialog(animateFrom: Dialog): TestDialog {
return runOnMainThreadAndWaitForIdleSync {
val dialog = TestDialog(context)
@@ -248,6 +318,22 @@
return result
}
+ private class TouchSurfaceView(context: Context) : FrameLayout(context), LaunchableView {
+ private val delegate =
+ LaunchableViewDelegate(
+ this,
+ superSetVisibility = { super.setVisibility(it) },
+ )
+
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {
+ delegate.setShouldBlockVisibilityChanges(block)
+ }
+
+ override fun setVisibility(visibility: Int) {
+ delegate.setVisibility(visibility)
+ }
+ }
+
private class TestDialog(context: Context) : Dialog(context) {
companion object {
const val DIALOG_WIDTH = 100
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
index 3696ec5..0798d73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
@@ -16,58 +16,34 @@
package com.android.systemui.animation
-import android.graphics.drawable.Drawable
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewParent
+import android.widget.FrameLayout
import androidx.test.filters.SmallTest
-import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.SysuiTestCase
-import org.junit.Before
+import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
- @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
- @Mock lateinit var view: View
- @Mock lateinit var rootView: ViewGroup
- @Mock lateinit var viewParent: ViewParent
- @Mock lateinit var drawable: Drawable
- lateinit var controller: GhostedViewLaunchAnimatorController
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- whenever(view.rootView).thenReturn(rootView)
- whenever(view.background).thenReturn(drawable)
- whenever(view.height).thenReturn(0)
- whenever(view.width).thenReturn(0)
- whenever(view.parent).thenReturn(viewParent)
- whenever(view.visibility).thenReturn(View.VISIBLE)
- whenever(view.invalidate()).then { /* NO-OP */ }
- whenever(view.getLocationOnScreen(any())).then { /* NO-OP */ }
- whenever(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
- whenever(interactionJankMonitor.end(anyInt())).thenReturn(true)
- controller = GhostedViewLaunchAnimatorController(view, 0, interactionJankMonitor)
- }
-
@Test
fun animatingOrphanViewDoesNotCrash() {
val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
+ val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext))
controller.onIntentStarted(willAnimate = true)
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
}
+
+ @Test
+ fun creatingControllerFromNormalViewThrows() {
+ assertThrows(IllegalArgumentException::class.java) {
+ GhostedViewLaunchAnimatorController(FrameLayout(mContext))
+ }
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
new file mode 100644
index 0000000..3bdbf97
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -0,0 +1,87 @@
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private data class BackInput(val progressX: Float, val progressY: Float, val edge: Int)
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BackAnimationSpecTest : SysuiTestCase() {
+ private var displayMetrics =
+ DisplayMetrics().apply {
+ widthPixels = 100
+ heightPixels = 200
+ density = 3f
+ }
+
+ @Test
+ fun sysUi_floatingSystemSurfaces_animationValues() {
+ val maxX = 14.0f
+ val maxY = 4.0f
+ val minScale = 0.8f
+
+ val backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics)
+
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 0f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+ expected = BackTransformation(translateX = 0f, translateY = 0f, scale = 1f),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+ expected = BackTransformation(translateX = -maxX, translateY = 0f, scale = minScale),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_RIGHT),
+ expected = BackTransformation(translateX = maxX, translateY = 0f, scale = minScale),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 1f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+ expected = BackTransformation(translateX = -maxX, translateY = -maxY, scale = minScale),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 0f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+ expected = BackTransformation(translateX = 0f, translateY = -maxY, scale = 1f),
+ )
+ assertBackTransformation(
+ backAnimationSpec = backAnimationSpec,
+ backInput = BackInput(progressX = 0f, progressY = -1f, edge = BackEvent.EDGE_LEFT),
+ expected = BackTransformation(translateX = 0f, translateY = maxY, scale = 1f),
+ )
+ }
+}
+
+private fun assertBackTransformation(
+ backAnimationSpec: BackAnimationSpec,
+ backInput: BackInput,
+ expected: BackTransformation,
+) {
+ val actual = BackTransformation()
+ backAnimationSpec.getBackTransformation(
+ backEvent =
+ BackEvent(
+ /* touchX = */ 0f,
+ /* touchY = */ 0f,
+ /* progress = */ backInput.progressX,
+ /* swipeEdge = */ backInput.edge,
+ ),
+ progressY = backInput.progressY,
+ result = actual
+ )
+
+ val tolerance = 0f
+ assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
+ assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+ assertThat(actual.scale).isWithin(tolerance).of(expected.scale)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
new file mode 100644
index 0000000..190b3d2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
@@ -0,0 +1,80 @@
+package com.android.systemui.animation.back
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BackTransformationTest : SysuiTestCase() {
+ private val targetView: View = mock()
+
+ @Test
+ fun defaultValue_noTransformation() {
+ val transformation = BackTransformation()
+
+ assertThat(transformation.translateX).isNaN()
+ assertThat(transformation.translateY).isNaN()
+ assertThat(transformation.scale).isNaN()
+ }
+
+ @Test
+ fun applyTo_targetView_translateX_Y_Scale() {
+ val transformation = BackTransformation(translateX = 0f, translateY = 0f, scale = 1f)
+
+ transformation.applyTo(targetView = targetView)
+
+ verify(targetView).translationX = 0f
+ verify(targetView).translationY = 0f
+ verify(targetView).scaleX = 1f
+ verify(targetView).scaleY = 1f
+ verifyNoMoreInteractions(targetView)
+ }
+
+ @Test
+ fun applyTo_targetView_translateX() {
+ val transformation = BackTransformation(translateX = 1f)
+
+ transformation.applyTo(targetView = targetView)
+
+ verify(targetView).translationX = 1f
+ verifyNoMoreInteractions(targetView)
+ }
+
+ @Test
+ fun applyTo_targetView_translateY() {
+ val transformation = BackTransformation(translateY = 2f)
+
+ transformation.applyTo(targetView = targetView)
+
+ verify(targetView).translationY = 2f
+ verifyNoMoreInteractions(targetView)
+ }
+
+ @Test
+ fun applyTo_targetView_scale() {
+ val transformation = BackTransformation(scale = 3f)
+
+ transformation.applyTo(targetView = targetView)
+
+ verify(targetView).scaleX = 3f
+ verify(targetView).scaleY = 3f
+ verifyNoMoreInteractions(targetView)
+ }
+
+ @Test
+ fun applyTo_targetView_noTransformation() {
+ val transformation = BackTransformation()
+
+ transformation.applyTo(targetView = targetView)
+
+ verifyNoMoreInteractions(targetView)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
new file mode 100644
index 0000000..921f9a8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
@@ -0,0 +1,63 @@
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class OnBackAnimationCallbackExtensionTest : SysuiTestCase() {
+ private val onBackProgress: (BackTransformation) -> Unit = mock()
+ private val onBackStart: (BackEvent) -> Unit = mock()
+ private val onBackInvoke: () -> Unit = mock()
+ private val onBackCancel: () -> Unit = mock()
+
+ private val displayMetrics =
+ DisplayMetrics().apply {
+ widthPixels = 100
+ heightPixels = 100
+ density = 1f
+ }
+
+ private val onBackAnimationCallback =
+ onBackAnimationCallbackFrom(
+ backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics),
+ displayMetrics = displayMetrics,
+ onBackProgressed = onBackProgress,
+ onBackStarted = onBackStart,
+ onBackInvoked = onBackInvoke,
+ onBackCancelled = onBackCancel,
+ )
+
+ @Test
+ fun onBackProgressed_shouldInvoke_onBackProgress() {
+ val backEvent = BackEvent(0f, 0f, 0f, BackEvent.EDGE_LEFT)
+ onBackAnimationCallback.onBackStarted(backEvent)
+
+ onBackAnimationCallback.onBackProgressed(backEvent)
+
+ verify(onBackProgress).invoke(BackTransformation(0f, 0f, 1f))
+ }
+
+ @Test
+ fun onBackStarted_shouldInvoke_onBackStart() {
+ val backEvent = BackEvent(0f, 0f, 0f, BackEvent.EDGE_LEFT)
+
+ onBackAnimationCallback.onBackStarted(backEvent)
+
+ verify(onBackStart).invoke(backEvent)
+ }
+
+ @Test
+ fun onBackInvoked_shouldInvoke_onBackInvoke() {
+ onBackAnimationCallback.onBackInvoked()
+
+ verify(onBackInvoke).invoke()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 83bf183..ace0ccb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -739,7 +739,7 @@
public void testForwardsDozeEvents() throws RemoteException {
when(mStatusBarStateController.isDozing()).thenReturn(true);
when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE);
- mAuthController.setBiometicContextListener(mContextListener);
+ mAuthController.setBiometricContextListener(mContextListener);
mStatusBarStateListenerCaptor.getValue().onDozingChanged(true);
mStatusBarStateListenerCaptor.getValue().onDozingChanged(false);
@@ -754,7 +754,7 @@
public void testForwardsWakeEvents() throws RemoteException {
when(mStatusBarStateController.isDozing()).thenReturn(false);
when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE);
- mAuthController.setBiometicContextListener(mContextListener);
+ mAuthController.setBiometricContextListener(mContextListener);
mWakefullnessObserverCaptor.getValue().onStartedGoingToSleep();
mWakefullnessObserverCaptor.getValue().onFinishedGoingToSleep();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index 3c40835..41beada 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -51,13 +51,22 @@
import android.view.WindowMetrics
import androidx.test.filters.SmallTest
import com.airbnb.lottie.LottieAnimationView
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.SysuiTestableContext
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.MODERN_ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -100,14 +109,16 @@
@Captor lateinit var overlayCaptor: ArgumentCaptor<View>
@Captor lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
+ private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
+ private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+ private val featureFlags = FakeFeatureFlags()
private val executor = FakeExecutor(FakeSystemClock())
private lateinit var overlayController: ISidefpsController
private lateinit var sideFpsController: SideFpsController
enum class DeviceConfig {
X_ALIGNED,
- Y_ALIGNED_UNFOLDED,
- Y_ALIGNED_FOLDED
+ Y_ALIGNED,
}
private lateinit var deviceConfig: DeviceConfig
@@ -121,6 +132,18 @@
@Before
fun setup() {
+ featureFlags.set(MODERN_ALTERNATE_BOUNCER, true)
+ keyguardBouncerRepository = FakeKeyguardBouncerRepository()
+ alternateBouncerInteractor =
+ AlternateBouncerInteractor(
+ keyguardBouncerRepository,
+ FakeBiometricSettingsRepository(),
+ FakeDeviceEntryFingerprintAuthRepository(),
+ FakeSystemClock(),
+ mock(KeyguardUpdateMonitor::class.java),
+ featureFlags,
+ )
+
context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, windowManager)
@@ -143,6 +166,7 @@
private fun testWithDisplay(
deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation: Boolean = false,
initInfo: DisplayInfo.() -> Unit = {},
windowInsets: WindowInsets = insetsForSmallNavbar(),
block: () -> Unit
@@ -151,27 +175,21 @@
when (deviceConfig) {
DeviceConfig.X_ALIGNED -> {
- displayWidth = 2560
- displayHeight = 1600
- sensorLocation = SensorLocationInternal("", 2325, 0, 0)
- boundsWidth = 160
- boundsHeight = 84
+ displayWidth = 3000
+ displayHeight = 1500
+ sensorLocation = SensorLocationInternal("", 2500, 0, 0)
+ boundsWidth = 200
+ boundsHeight = 100
}
- DeviceConfig.Y_ALIGNED_UNFOLDED -> {
- displayWidth = 2208
- displayHeight = 1840
- sensorLocation = SensorLocationInternal("", 0, 510, 0)
- boundsWidth = 110
- boundsHeight = 210
- }
- DeviceConfig.Y_ALIGNED_FOLDED -> {
- displayWidth = 1080
- displayHeight = 2100
- sensorLocation = SensorLocationInternal("", 0, 590, 0)
- boundsWidth = 110
- boundsHeight = 210
+ DeviceConfig.Y_ALIGNED -> {
+ displayWidth = 2500
+ displayHeight = 2000
+ sensorLocation = SensorLocationInternal("", 0, 300, 0)
+ boundsWidth = 100
+ boundsHeight = 200
}
}
+
indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
displayBounds = Rect(0, 0, displayWidth, displayHeight)
var locations = listOf(sensorLocation)
@@ -194,8 +212,10 @@
val displayInfo = DisplayInfo()
displayInfo.initInfo()
+
val dmGlobal = mock(DisplayManagerGlobal::class.java)
val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS)
+
whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo)
whenEver(windowManager.defaultDisplay).thenReturn(display)
whenEver(windowManager.maximumWindowMetrics)
@@ -203,9 +223,15 @@
whenEver(windowManager.currentWindowMetrics)
.thenReturn(WindowMetrics(displayBounds, windowInsets))
+ val sideFpsControllerContext = context.createDisplayContext(display) as SysuiTestableContext
+ sideFpsControllerContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_reverseDefaultRotation,
+ isReverseDefaultRotation
+ )
+
sideFpsController =
SideFpsController(
- context.createDisplayContext(display),
+ sideFpsControllerContext,
layoutInflater,
fingerprintManager,
windowManager,
@@ -214,7 +240,10 @@
displayManager,
executor,
handler,
- dumpManager
+ alternateBouncerInteractor,
+ TestCoroutineScope(),
+ featureFlags,
+ dumpManager,
)
overlayController =
@@ -299,82 +328,235 @@
}
@Test
- fun showsWithTaskbar() =
- testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_0 }) {
- hidesWithTaskbar(visible = true)
- }
-
- @Test
- fun showsWithTaskbarOnY() =
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_0() =
testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_0 }
- ) { hidesWithTaskbar(visible = true) }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
@Test
- fun showsWithTaskbar90() =
- testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_90 }) {
- hidesWithTaskbar(visible = true)
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_90() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_90 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_180 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_180 },
+ windowInsets = insetsForSmallNavbar()
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_180 },
+ windowInsets = insetsForLargeNavbar()
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_270 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_0 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_90() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_90 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_InReverseDefaultRotation_90() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_90 },
+ windowInsets = insetsForSmallNavbar()
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_InReverseDefaultRotation_90() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_90 },
+ windowInsets = insetsForLargeNavbar()
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_180 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_90() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_90 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_180 },
+ ) {
+ verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
}
@Test
- fun showsWithTaskbar90OnY() =
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_270() =
testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
- { rotation = Surface.ROTATION_90 }
- ) { hidesWithTaskbar(visible = true) }
-
- @Test
- fun showsWithTaskbar180() =
- testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
- { rotation = Surface.ROTATION_180 }
- ) { hidesWithTaskbar(visible = true) }
-
- @Test
- fun showsWithTaskbar270OnY() =
- testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_270 }
- ) { hidesWithTaskbar(visible = true) }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
@Test
- fun showsWithTaskbarCollapsedDown() =
+ fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_270() =
testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_270 },
windowInsets = insetsForSmallNavbar()
- ) { hidesWithTaskbar(visible = true) }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
@Test
- fun showsWithTaskbarCollapsedDownOnY() =
+ fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_270() =
testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
- { rotation = Surface.ROTATION_180 },
- windowInsets = insetsForSmallNavbar()
- ) { hidesWithTaskbar(visible = true) }
-
- @Test
- fun hidesWithTaskbarDown() =
- testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
- { rotation = Surface.ROTATION_180 },
- windowInsets = insetsForLargeNavbar()
- ) { hidesWithTaskbar(visible = false) }
-
- @Test
- fun hidesWithTaskbarDownOnY() =
- testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_270 },
windowInsets = insetsForLargeNavbar()
- ) { hidesWithTaskbar(visible = true) }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_0 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_90() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_90 },
+ ) {
+ verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
+ }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_180 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_InReverseDefaultRotation_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_180 },
+ windowInsets = insetsForSmallNavbar()
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ @Test
+ fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_InReverseDefaultRotation_180() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_180 },
+ windowInsets = insetsForLargeNavbar()
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false) }
+
+ @Test
+ fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) { verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true) }
+
+ private fun verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible: Boolean) {
+ sideFpsController.overlayOffsets = sensorLocation
+ }
+
+ fun alternateBouncerVisibility_showAndHideSideFpsUI() = testWithDisplay {
+ // WHEN alternate bouncer is visible
+ keyguardBouncerRepository.setAlternateVisible(true)
+ executor.runAllReady()
+
+ // THEN side fps shows UI
+ verify(windowManager).addView(any(), any())
+ verify(windowManager, never()).removeView(any())
+
+ // WHEN alternate bouncer is no longer visible
+ keyguardBouncerRepository.setAlternateVisible(false)
+ executor.runAllReady()
+
+ // THEN side fps UI is hidden
+ verify(windowManager).removeView(any())
+ }
private fun hidesWithTaskbar(visible: Boolean) {
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
- sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(visible, false)
+ sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(true, false)
executor.runAllReady()
verify(windowManager).addView(any(), any())
@@ -382,25 +564,100 @@
verify(sideFpsView).visibility = if (visible) View.VISIBLE else View.GONE
}
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
@Test
- fun testIndicatorPlacementForXAlignedSensor() =
- testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED) {
- overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ fun verifiesIndicatorPlacementForXAlignedSensor_0() {
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
sideFpsController.overlayOffsets = sensorLocation
+
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
-
assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
}
+ }
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
@Test
- fun testIndicatorPlacementForYAlignedSensor() =
- testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+ fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() {
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
sideFpsController.overlayOffsets = sensorLocation
+
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
@@ -412,7 +669,6 @@
@Test
fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
// By default all those tests assume the side fps sensor is available.
-
assertThat(fingerprintManager.hasSideFpsSensor()).isTrue()
}
@@ -425,7 +681,7 @@
@Test
fun testLayoutParams_isKeyguardDialogType() =
- testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
@@ -440,7 +696,7 @@
@Test
fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
- testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
@@ -455,7 +711,7 @@
@Test
fun testLayoutParams_hasTrustedOverlayWindowFlag() =
- testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 53bc2c2..0690d1b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -43,6 +43,7 @@
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionStateManager
@@ -52,7 +53,7 @@
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
@@ -95,18 +96,19 @@
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var transitionController: LockscreenShadeTransitionController
@Mock private lateinit var configurationController: ConfigurationController
- @Mock private lateinit var systemClock: SystemClock
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var unlockedScreenOffAnimationController:
UnlockedScreenOffAnimationController
@Mock private lateinit var udfpsDisplayMode: UdfpsDisplayModeProvider
+ @Mock private lateinit var secureSettings: SecureSettings
@Mock private lateinit var controllerCallback: IUdfpsOverlayControllerCallback
@Mock private lateinit var udfpsController: UdfpsController
@Mock private lateinit var udfpsView: UdfpsView
@Mock private lateinit var udfpsEnrollView: UdfpsEnrollView
@Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@Mock private lateinit var featureFlags: FeatureFlags
- @Mock private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
+ @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+ @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -138,10 +140,10 @@
context, fingerprintManager, inflater, windowManager, accessibilityManager,
statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
- configurationController, systemClock, keyguardStateController,
- unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
- controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
- mPrimaryBouncerInteractor, isDebuggable
+ configurationController, keyguardStateController,
+ unlockedScreenOffAnimationController, udfpsDisplayMode, secureSettings, REQUEST_ID,
+ reason, controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
+ primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable,
)
block()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index b061eb3..232daad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -81,6 +81,7 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -95,6 +96,7 @@
import com.android.systemui.util.concurrency.Execution;
import com.android.systemui.util.concurrency.FakeExecution;
import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.util.time.SystemClock;
@@ -202,6 +204,10 @@
private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
@Mock
private SinglePointerTouchProcessor mSinglePointerTouchProcessor;
+ @Mock
+ private AlternateBouncerInteractor mAlternateBouncerInteractor;
+ @Mock
+ private SecureSettings mSecureSettings;
// Capture listeners so that they can be used to send events
@Captor
@@ -292,7 +298,8 @@
mDisplayManager, mHandler, mConfigurationController, mSystemClock,
mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker,
mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor,
- mPrimaryBouncerInteractor, mSinglePointerTouchProcessor);
+ mPrimaryBouncerInteractor, mSinglePointerTouchProcessor,
+ mAlternateBouncerInteractor, mSecureSettings);
verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
mOverlayController = mOverlayCaptor.getValue();
verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
@@ -406,7 +413,7 @@
// GIVEN overlay was showing and the udfps bouncer is showing
mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
- when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
// WHEN the overlay is hidden
mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
index 3c61382..dbbc266 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
@@ -30,20 +30,19 @@
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shade.ShadeExpansionListener;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
@@ -71,11 +70,10 @@
protected @Mock SystemUIDialogManager mDialogManager;
protected @Mock UdfpsController mUdfpsController;
protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
- protected @Mock KeyguardBouncer mBouncer;
protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+ protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
- protected FakeSystemClock mSystemClock = new FakeSystemClock();
protected UdfpsKeyguardViewController mController;
@@ -86,10 +84,6 @@
private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
protected List<ShadeExpansionListener> mExpansionListeners;
- private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateBouncer>
- mAlternateBouncerCaptor;
- protected StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
-
private @Captor ArgumentCaptor<KeyguardStateController.Callback>
mKeyguardStateControllerCallbackCaptor;
protected KeyguardStateController.Callback mKeyguardStateControllerCallback;
@@ -135,12 +129,6 @@
}
}
- protected void captureAltAuthInterceptor() {
- verify(mStatusBarKeyguardViewManager).setAlternateBouncer(
- mAlternateBouncerCaptor.capture());
- mAlternateBouncer = mAlternateBouncerCaptor.getValue();
- }
-
protected void captureKeyguardStateControllerCallback() {
verify(mKeyguardStateController).addCallback(
mKeyguardStateControllerCallbackCaptor.capture());
@@ -159,10 +147,8 @@
protected UdfpsKeyguardViewController createUdfpsKeyguardViewController(
boolean useModernBouncer, boolean useExpandedOverlay) {
- mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer);
+ mFeatureFlags.set(Flags.MODERN_ALTERNATE_BOUNCER, useModernBouncer);
mFeatureFlags.set(Flags.UDFPS_NEW_TOUCH_DETECTION, useExpandedOverlay);
- when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn(
- useModernBouncer ? null : mBouncer);
UdfpsKeyguardViewController controller = new UdfpsKeyguardViewController(
mView,
mStatusBarStateController,
@@ -172,14 +158,14 @@
mDumpManager,
mLockscreenShadeTransitionController,
mConfigurationController,
- mSystemClock,
mKeyguardStateController,
mUnlockedScreenOffAnimationController,
mDialogManager,
mUdfpsController,
mActivityLaunchAnimator,
mFeatureFlags,
- mPrimaryBouncerInteractor);
+ mPrimaryBouncerInteractor,
+ mAlternateBouncerInteractor);
return controller;
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index babe533..f437a8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -19,39 +19,30 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
+import android.testing.TestableLooper;
import android.view.MotionEvent;
import androidx.test.filters.SmallTest;
import com.android.systemui.shade.ShadeExpansionListener;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
-public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest {
- private @Captor ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback>
- mBouncerExpansionCallbackCaptor;
- private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest {
@Override
public UdfpsKeyguardViewController createUdfpsKeyguardViewController() {
return createUdfpsKeyguardViewController(/* useModernBouncer */ false,
@@ -64,16 +55,12 @@
captureStatusBarStateListeners();
sendStatusBarStateChanged(StatusBarState.KEYGUARD);
- captureBouncerExpansionCallback();
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true);
- mBouncerExpansionCallback.onVisibilityChanged(true);
-
+ when(mView.getUnpausedAlpha()).thenReturn(0);
assertTrue(mController.shouldPauseAuth());
}
-
-
@Test
public void testRegistersExpansionChangedListenerOnAttached() {
mController.onViewAttached();
@@ -237,85 +224,9 @@
public void testOverrideShouldPauseAuthOnShadeLocked() {
mController.onViewAttached();
captureStatusBarStateListeners();
- captureAltAuthInterceptor();
sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED);
assertTrue(mController.shouldPauseAuth());
-
- mAlternateBouncer.showAlternateBouncer(); // force show
- assertFalse(mController.shouldPauseAuth());
- assertTrue(mAlternateBouncer.isShowingAlternateBouncer());
-
- mAlternateBouncer.hideAlternateBouncer(); // stop force show
- assertTrue(mController.shouldPauseAuth());
- assertFalse(mAlternateBouncer.isShowingAlternateBouncer());
- }
-
- @Test
- public void testOnDetachedStateReset() {
- // GIVEN view is attached
- mController.onViewAttached();
- captureAltAuthInterceptor();
-
- // WHEN view is detached
- mController.onViewDetached();
-
- // THEN remove alternate auth interceptor
- verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAlternateBouncer);
- }
-
- @Test
- public void testHiddenUdfpsBouncerOnTouchOutside_nothingHappens() {
- // GIVEN view is attached
- mController.onViewAttached();
- captureAltAuthInterceptor();
-
- // GIVEN udfps bouncer isn't showing
- mAlternateBouncer.hideAlternateBouncer();
-
- // WHEN touch is observed outside the view
- mController.onTouchOutsideView();
-
- // THEN bouncer / alt auth methods are never called
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
- verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
- verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean());
- }
-
- @Test
- public void testShowingUdfpsBouncerOnTouchOutsideWithinThreshold_nothingHappens() {
- // GIVEN view is attached
- mController.onViewAttached();
- captureAltAuthInterceptor();
-
- // GIVEN udfps bouncer is showing
- mAlternateBouncer.showAlternateBouncer();
-
- // WHEN touch is observed outside the view 200ms later (just within threshold)
- mSystemClock.advanceTime(200);
- mController.onTouchOutsideView();
-
- // THEN bouncer / alt auth methods are never called because not enough time has passed
- verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
- verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
- verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean());
- }
-
- @Test
- public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showPrimaryBouncer() {
- // GIVEN view is attached
- mController.onViewAttached();
- captureAltAuthInterceptor();
-
- // GIVEN udfps bouncer is showing
- mAlternateBouncer.showAlternateBouncer();
-
- // WHEN touch is observed outside the view 205ms later
- mSystemClock.advanceTime(205);
- mController.onTouchOutsideView();
-
- // THEN show the bouncer
- verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(eq(true));
}
@Test
@@ -334,25 +245,6 @@
}
@Test
- public void testShowUdfpsBouncer() {
- // GIVEN view is attached and status bar expansion is 0
- mController.onViewAttached();
- captureStatusBarExpansionListeners();
- captureKeyguardStateControllerCallback();
- captureAltAuthInterceptor();
- updateStatusBarExpansion(0, true);
- reset(mView);
- when(mView.getContext()).thenReturn(mResourceContext);
- when(mResourceContext.getString(anyInt())).thenReturn("test string");
-
- // WHEN status bar expansion is 0 but udfps bouncer is requested
- mAlternateBouncer.showAlternateBouncer();
-
- // THEN alpha is 255
- verify(mView).setUnpausedAlpha(255);
- }
-
- @Test
public void testTransitionToFullShadeProgress() {
// GIVEN view is attached and status bar expansion is 1f
mController.onViewAttached();
@@ -370,24 +262,6 @@
}
@Test
- public void testShowUdfpsBouncer_transitionToFullShadeProgress() {
- // GIVEN view is attached and status bar expansion is 1f
- mController.onViewAttached();
- captureStatusBarExpansionListeners();
- captureKeyguardStateControllerCallback();
- captureAltAuthInterceptor();
- updateStatusBarExpansion(1f, true);
- mAlternateBouncer.showAlternateBouncer();
- reset(mView);
-
- // WHEN we're transitioning to the full shade
- mController.setTransitionToFullShadeProgress(1.0f);
-
- // THEN alpha is 255 (b/c udfps bouncer is requested)
- verify(mView).setUnpausedAlpha(255);
- }
-
- @Test
public void testUpdatePanelExpansion_pauseAuth() {
// GIVEN view is attached + on the keyguard
mController.onViewAttached();
@@ -421,11 +295,6 @@
verify(mView, atLeastOnce()).setPauseAuth(false);
}
- private void captureBouncerExpansionCallback() {
- verify(mBouncer).addBouncerExpansionCallback(mBouncerExpansionCallbackCaptor.capture());
- mBouncerExpansionCallback = mBouncerExpansionCallbackCaptor.getValue();
- }
-
@Test
// TODO(b/259264861): Tracking Bug
public void testUdfpsExpandedOverlayOn() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
index 2d412dc..c73ff1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
@@ -21,26 +21,37 @@
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.phone.KeyguardBouncer
import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.yield
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
import org.mockito.Mock
import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@@ -55,8 +66,9 @@
allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
MockitoAnnotations.initMocks(this)
keyguardBouncerRepository =
- KeyguardBouncerRepository(
+ KeyguardBouncerRepositoryImpl(
mock(com.android.keyguard.ViewMediatorCallback::class.java),
+ FakeSystemClock(),
TestCoroutineScope(),
bouncerLogger,
)
@@ -77,15 +89,44 @@
mock(KeyguardBypassController::class.java),
mKeyguardUpdateMonitor
)
+ mAlternateBouncerInteractor =
+ AlternateBouncerInteractor(
+ keyguardBouncerRepository,
+ mock(BiometricSettingsRepository::class.java),
+ mock(DeviceEntryFingerprintAuthRepository::class.java),
+ mock(SystemClock::class.java),
+ mock(KeyguardUpdateMonitor::class.java),
+ mock(FeatureFlags::class.java)
+ )
return createUdfpsKeyguardViewController(
/* useModernBouncer */ true, /* useExpandedOverlay */
false
)
}
- /** After migration, replaces LockIconViewControllerTest version */
@Test
- fun testShouldPauseAuthBouncerShowing() =
+ fun shadeLocked_showAlternateBouncer_unpauseAuth() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN view is attached + on the SHADE_LOCKED (udfps view not showing)
+ mController.onViewAttached()
+ captureStatusBarStateListeners()
+ sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED)
+
+ // WHEN alternate bouncer is requested
+ val job = mController.listenForAlternateBouncerVisibility(this)
+ keyguardBouncerRepository.setAlternateVisible(true)
+ yield()
+
+ // THEN udfps view will animate in & pause auth is updated to NOT pause
+ verify(mView).animateInUdfpsBouncer(any())
+ assertFalse(mController.shouldPauseAuth())
+
+ job.cancel()
+ }
+
+ /** After migration to MODERN_BOUNCER, replaces UdfpsKeyguardViewControllerTest version */
+ @Test
+ fun shouldPauseAuthBouncerShowing() =
runBlocking(IMMEDIATE) {
// GIVEN view attached and we're on the keyguard
mController.onViewAttached()
@@ -95,7 +136,7 @@
// WHEN the bouncer expansion is VISIBLE
val job = mController.listenForBouncerExpansion(this)
keyguardBouncerRepository.setPrimaryVisible(true)
- keyguardBouncerRepository.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+ keyguardBouncerRepository.setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
yield()
// THEN UDFPS shouldPauseAuth == true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index d550b92..8255a14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -80,15 +80,6 @@
}
@Test
- fun forwardsEvents() {
- view.dozeTimeTick()
- verify(animationViewController).dozeTimeTick()
-
- view.onTouchOutsideView()
- verify(animationViewController).onTouchOutsideView()
- }
-
- @Test
fun layoutSizeFitsSensor() {
val params = withArgCaptor<RectF> {
verify(animationViewController).onSensorRectUpdated(capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
new file mode 100644
index 0000000..af46d9b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.udfps
+
+import android.graphics.Point
+import android.graphics.Rect
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.`when` as whenEver
+
+@SmallTest
+@RunWith(Parameterized::class)
+class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() {
+ val underTest = spy(EllipseOverlapDetector(neededPoints = 1))
+
+ @Before
+ fun setUp() {
+ // Use one single center point for testing, required or total number of points may change
+ whenEver(underTest.calculateSensorPoints(SENSOR))
+ .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY())))
+ }
+
+ @Test
+ fun isGoodOverlap() {
+ val touchData =
+ TOUCH_DATA.copy(
+ x = testCase.x.toFloat(),
+ y = testCase.y.toFloat(),
+ minor = testCase.minor,
+ major = testCase.major
+ )
+ val actual = underTest.isGoodOverlap(touchData, SENSOR)
+
+ assertThat(actual).isEqualTo(testCase.expected)
+ }
+
+ data class TestCase(
+ val x: Int,
+ val y: Int,
+ val minor: Float,
+ val major: Float,
+ val expected: Boolean
+ )
+
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun data(): List<TestCase> =
+ listOf(
+ genTestCases(
+ innerXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()),
+ innerYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()),
+ outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
+ outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
+ minor = 300f,
+ major = 300f,
+ expected = true
+ ),
+ genTestCases(
+ innerXs = listOf(SENSOR.left, SENSOR.right),
+ innerYs = listOf(SENSOR.top, SENSOR.bottom),
+ outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
+ outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
+ minor = 100f,
+ major = 100f,
+ expected = false
+ )
+ )
+ .flatten()
+ }
+}
+
+/* Placeholder touch parameters. */
+private const val POINTER_ID = 42
+private const val NATIVE_MINOR = 2.71828f
+private const val NATIVE_MAJOR = 3.14f
+private const val ORIENTATION = 0f // used for perfect circles
+private const val TIME = 12345699L
+private const val GESTURE_START = 12345600L
+
+/* Template [NormalizedTouchData]. */
+private val TOUCH_DATA =
+ NormalizedTouchData(
+ POINTER_ID,
+ x = 0f,
+ y = 0f,
+ NATIVE_MINOR,
+ NATIVE_MAJOR,
+ ORIENTATION,
+ TIME,
+ GESTURE_START
+ )
+
+private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */)
+
+private fun genTestCases(
+ innerXs: List<Int>,
+ innerYs: List<Int>,
+ outerXs: List<Int>,
+ outerYs: List<Int>,
+ minor: Float,
+ major: Float,
+ expected: Boolean
+): List<EllipseOverlapDetectorTest.TestCase> {
+ return (innerXs + outerXs).flatMap { x ->
+ (innerYs + outerYs).map { y ->
+ EllipseOverlapDetectorTest.TestCase(x, y, minor, major, expected)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
index 95c53b4..8e20303 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
@@ -39,7 +39,8 @@
@Test
fun processTouch() {
- overlapDetector.shouldReturn = testCase.isGoodOverlap
+ overlapDetector.shouldReturn =
+ testCase.currentPointers.associate { pointer -> pointer.id to pointer.onSensor }
val actual =
underTest.processTouch(
@@ -56,7 +57,7 @@
data class TestCase(
val event: MotionEvent,
- val isGoodOverlap: Boolean,
+ val currentPointers: List<TestPointer>,
val previousPointerOnSensorId: Int,
val overlayParams: UdfpsOverlayParams,
val expected: TouchProcessorResult,
@@ -91,28 +92,43 @@
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = true,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.DOWN,
- expectedPointerOnSensorId = POINTER_ID,
- ),
- genPositiveTestCases(
- motionEventAction = MotionEvent.ACTION_DOWN,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = true,
- expectedInteractionEvent = InteractionEvent.DOWN,
- expectedPointerOnSensorId = POINTER_ID,
+ expectedPointerOnSensorId = POINTER_ID_1,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = false,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = false,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+ expectedInteractionEvent = InteractionEvent.UP,
+ expectedPointerOnSensorId = INVALID_POINTER_ID,
+ ),
+ // MotionEvent.ACTION_HOVER_ENTER
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_ENTER,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+ expectedInteractionEvent = InteractionEvent.DOWN,
+ expectedPointerOnSensorId = POINTER_ID_1,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_ENTER,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = INVALID_POINTER_ID,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_ENTER,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
@@ -120,28 +136,79 @@
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = true,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.DOWN,
- expectedPointerOnSensorId = POINTER_ID,
+ expectedPointerOnSensorId = POINTER_ID_1,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = true,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.UNCHANGED,
- expectedPointerOnSensorId = POINTER_ID,
+ expectedPointerOnSensorId = POINTER_ID_1,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = false,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = false,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+ expectedInteractionEvent = InteractionEvent.UP,
+ expectedPointerOnSensorId = INVALID_POINTER_ID,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_MOVE,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = false),
+ TestPointer(id = POINTER_ID_2, onSensor = true)
+ ),
+ expectedInteractionEvent = InteractionEvent.DOWN,
+ expectedPointerOnSensorId = POINTER_ID_2,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_MOVE,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = false),
+ TestPointer(id = POINTER_ID_2, onSensor = true)
+ ),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = POINTER_ID_2,
+ ),
+ // MotionEvent.ACTION_HOVER_MOVE
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+ expectedInteractionEvent = InteractionEvent.DOWN,
+ expectedPointerOnSensorId = POINTER_ID_1,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = POINTER_ID_1,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = INVALID_POINTER_ID,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_MOVE,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
@@ -149,78 +216,197 @@
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = true,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = true,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = false,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
+ // MotionEvent.ACTION_HOVER_EXIT
genPositiveTestCases(
- motionEventAction = MotionEvent.ACTION_UP,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = false,
+ motionEventAction = MotionEvent.ACTION_HOVER_EXIT,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_EXIT,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
+ expectedInteractionEvent = InteractionEvent.UP,
+ expectedPointerOnSensorId = INVALID_POINTER_ID,
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_HOVER_EXIT,
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = INVALID_POINTER_ID,
+ ),
// MotionEvent.ACTION_CANCEL
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = true,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = true,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = true)),
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
previousPointerOnSensorId = INVALID_POINTER_ID,
- isGoodOverlap = false,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
- previousPointerOnSensorId = POINTER_ID,
- isGoodOverlap = false,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = false)),
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
- )
- .flatten() +
- listOf(
- // Unsupported MotionEvent actions.
- genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_DOWN),
- genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_UP),
- genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_ENTER),
- genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_MOVE),
- genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_EXIT),
+ // MotionEvent.ACTION_POINTER_DOWN
+ genPositiveTestCases(
+ motionEventAction =
+ MotionEvent.ACTION_POINTER_DOWN +
+ (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = true),
+ TestPointer(id = POINTER_ID_2, onSensor = false)
+ ),
+ expectedInteractionEvent = InteractionEvent.DOWN,
+ expectedPointerOnSensorId = POINTER_ID_1,
+ ),
+ genPositiveTestCases(
+ motionEventAction =
+ MotionEvent.ACTION_POINTER_DOWN +
+ (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = false),
+ TestPointer(id = POINTER_ID_2, onSensor = true)
+ ),
+ expectedInteractionEvent = InteractionEvent.DOWN,
+ expectedPointerOnSensorId = POINTER_ID_2
+ ),
+ genPositiveTestCases(
+ motionEventAction =
+ MotionEvent.ACTION_POINTER_DOWN +
+ (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = true),
+ TestPointer(id = POINTER_ID_2, onSensor = false)
+ ),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = POINTER_ID_1,
+ ),
+ // MotionEvent.ACTION_POINTER_UP
+ genPositiveTestCases(
+ motionEventAction =
+ MotionEvent.ACTION_POINTER_UP +
+ (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ previousPointerOnSensorId = INVALID_POINTER_ID,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = false),
+ TestPointer(id = POINTER_ID_2, onSensor = false)
+ ),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = INVALID_POINTER_ID
+ ),
+ genPositiveTestCases(
+ motionEventAction =
+ MotionEvent.ACTION_POINTER_UP +
+ (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ previousPointerOnSensorId = POINTER_ID_2,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = false),
+ TestPointer(id = POINTER_ID_2, onSensor = true)
+ ),
+ expectedInteractionEvent = InteractionEvent.UP,
+ expectedPointerOnSensorId = INVALID_POINTER_ID
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_POINTER_UP,
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = true),
+ TestPointer(id = POINTER_ID_2, onSensor = false)
+ ),
+ expectedInteractionEvent = InteractionEvent.UP,
+ expectedPointerOnSensorId = INVALID_POINTER_ID
+ ),
+ genPositiveTestCases(
+ motionEventAction =
+ MotionEvent.ACTION_POINTER_UP +
+ (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ previousPointerOnSensorId = POINTER_ID_1,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = true),
+ TestPointer(id = POINTER_ID_2, onSensor = false)
+ ),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = POINTER_ID_1
+ ),
+ genPositiveTestCases(
+ motionEventAction = MotionEvent.ACTION_POINTER_UP,
+ previousPointerOnSensorId = POINTER_ID_2,
+ currentPointers =
+ listOf(
+ TestPointer(id = POINTER_ID_1, onSensor = false),
+ TestPointer(id = POINTER_ID_2, onSensor = true)
+ ),
+ expectedInteractionEvent = InteractionEvent.UNCHANGED,
+ expectedPointerOnSensorId = POINTER_ID_2
)
- .flatten()
+ )
+ .flatten()
}
}
+data class TestPointer(val id: Int, val onSensor: Boolean)
+
/* Display dimensions in native resolution and natural orientation. */
private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400
private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600
+/* Placeholder touch parameters. */
+private const val POINTER_ID_1 = 42
+private const val POINTER_ID_2 = 43
+private const val NATIVE_MINOR = 2.71828f
+private const val NATIVE_MAJOR = 3.14f
+private const val ORIENTATION = 1.2345f
+private const val TIME = 12345699L
+private const val GESTURE_START = 12345600L
+
/*
* ROTATION_0 map:
* _ _ _ _
@@ -244,6 +430,7 @@
private val ROTATION_0_INPUTS =
OrientationBasedInputs(
rotation = Surface.ROTATION_0,
+ nativeOrientation = ORIENTATION,
nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(),
nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(),
nativeXOutsideSensor = 250f,
@@ -271,6 +458,7 @@
private val ROTATION_90_INPUTS =
OrientationBasedInputs(
rotation = Surface.ROTATION_90,
+ nativeOrientation = (ORIENTATION - Math.PI.toFloat() / 2),
nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(),
nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(),
nativeXOutsideSensor = 150f,
@@ -304,25 +492,18 @@
private val ROTATION_270_INPUTS =
OrientationBasedInputs(
rotation = Surface.ROTATION_270,
+ nativeOrientation = (ORIENTATION + Math.PI.toFloat() / 2),
nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(),
nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(),
nativeXOutsideSensor = 450f,
nativeYOutsideSensor = 250f,
)
-/* Placeholder touch parameters. */
-private const val POINTER_ID = 42
-private const val NATIVE_MINOR = 2.71828f
-private const val NATIVE_MAJOR = 3.14f
-private const val ORIENTATION = 1.23f
-private const val TIME = 12345699L
-private const val GESTURE_START = 12345600L
-
/* Template [MotionEvent]. */
private val MOTION_EVENT =
obtainMotionEvent(
action = 0,
- pointerId = POINTER_ID,
+ pointerId = POINTER_ID_1,
x = 0f,
y = 0f,
minor = 0f,
@@ -335,7 +516,7 @@
/* Template [NormalizedTouchData]. */
private val NORMALIZED_TOUCH_DATA =
NormalizedTouchData(
- POINTER_ID,
+ POINTER_ID_1,
x = 0f,
y = 0f,
NATIVE_MINOR,
@@ -352,6 +533,7 @@
*/
private data class OrientationBasedInputs(
@Rotation val rotation: Int,
+ val nativeOrientation: Float,
val nativeXWithinSensor: Float,
val nativeYWithinSensor: Float,
val nativeXOutsideSensor: Float,
@@ -380,7 +562,7 @@
private fun genPositiveTestCases(
motionEventAction: Int,
previousPointerOnSensorId: Int,
- isGoodOverlap: Boolean,
+ currentPointers: List<TestPointer>,
expectedInteractionEvent: InteractionEvent,
expectedPointerOnSensorId: Int
): List<SinglePointerTouchProcessorTest.TestCase> {
@@ -395,21 +577,47 @@
return scaleFactors.flatMap { scaleFactor ->
orientations.map { orientation ->
val overlayParams = orientation.toOverlayParams(scaleFactor)
- val nativeX = orientation.getNativeX(isGoodOverlap)
- val nativeY = orientation.getNativeY(isGoodOverlap)
+
+ val pointerProperties =
+ currentPointers
+ .map { pointer ->
+ val pp = MotionEvent.PointerProperties()
+ pp.id = pointer.id
+ pp
+ }
+ .toList()
+
+ val pointerCoords =
+ currentPointers
+ .map { pointer ->
+ val pc = MotionEvent.PointerCoords()
+ pc.x = orientation.getNativeX(pointer.onSensor) * scaleFactor
+ pc.y = orientation.getNativeY(pointer.onSensor) * scaleFactor
+ pc.touchMinor = NATIVE_MINOR * scaleFactor
+ pc.touchMajor = NATIVE_MAJOR * scaleFactor
+ pc.orientation = orientation.nativeOrientation
+ pc
+ }
+ .toList()
+
val event =
MOTION_EVENT.copy(
action = motionEventAction,
- x = nativeX * scaleFactor,
- y = nativeY * scaleFactor,
- minor = NATIVE_MINOR * scaleFactor,
- major = NATIVE_MAJOR * scaleFactor,
+ pointerProperties = pointerProperties,
+ pointerCoords = pointerCoords
)
+
val expectedTouchData =
- NORMALIZED_TOUCH_DATA.copy(
- x = ROTATION_0_INPUTS.getNativeX(isGoodOverlap),
- y = ROTATION_0_INPUTS.getNativeY(isGoodOverlap),
- )
+ if (expectedPointerOnSensorId != INVALID_POINTER_ID) {
+ NORMALIZED_TOUCH_DATA.copy(
+ pointerId = expectedPointerOnSensorId,
+ x = ROTATION_0_INPUTS.getNativeX(isWithinSensor = true),
+ y = ROTATION_0_INPUTS.getNativeY(isWithinSensor = true)
+ )
+ } else {
+ NormalizedTouchData()
+ }
+
val expected =
TouchProcessorResult.ProcessedTouch(
event = expectedInteractionEvent,
@@ -418,7 +626,7 @@
)
SinglePointerTouchProcessorTest.TestCase(
event = event,
- isGoodOverlap = isGoodOverlap,
+ currentPointers = currentPointers,
previousPointerOnSensorId = previousPointerOnSensorId,
overlayParams = overlayParams,
expected = expected,
@@ -431,7 +639,7 @@
motionEventAction: Int
): List<SinglePointerTouchProcessorTest.TestCase> {
val isGoodOverlap = true
- val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID)
+ val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID_1)
return previousPointerOnSensorIds.map { previousPointerOnSensorId ->
val overlayParams = ROTATION_0_INPUTS.toOverlayParams(scaleFactor = 1f)
val nativeX = ROTATION_0_INPUTS.getNativeX(isGoodOverlap)
@@ -446,7 +654,7 @@
)
SinglePointerTouchProcessorTest.TestCase(
event = event,
- isGoodOverlap = isGoodOverlap,
+ currentPointers = listOf(TestPointer(id = POINTER_ID_1, onSensor = isGoodOverlap)),
previousPointerOnSensorId = previousPointerOnSensorId,
overlayParams = overlayParams,
expected = TouchProcessorResult.Failure(),
@@ -473,13 +681,23 @@
pc.touchMinor = minor
pc.touchMajor = major
pc.orientation = orientation
+ return obtainMotionEvent(action, arrayOf(pp), arrayOf(pc), time, gestureStart)
+}
+
+private fun obtainMotionEvent(
+ action: Int,
+ pointerProperties: Array<MotionEvent.PointerProperties>,
+ pointerCoords: Array<MotionEvent.PointerCoords>,
+ time: Long,
+ gestureStart: Long,
+): MotionEvent {
return MotionEvent.obtain(
gestureStart /* downTime */,
time /* eventTime */,
action /* action */,
- 1 /* pointerCount */,
- arrayOf(pp) /* pointerProperties */,
- arrayOf(pc) /* pointerCoords */,
+ pointerCoords.size /* pointerCount */,
+ pointerProperties /* pointerProperties */,
+ pointerCoords /* pointerCoords */,
0 /* metaState */,
0 /* buttonState */,
1f /* xPrecision */,
@@ -503,4 +721,19 @@
gestureStart: Long = this.downTime,
) = obtainMotionEvent(action, pointerId, x, y, minor, major, orientation, time, gestureStart)
+private fun MotionEvent.copy(
+ action: Int = this.action,
+ pointerProperties: List<MotionEvent.PointerProperties>,
+ pointerCoords: List<MotionEvent.PointerCoords>,
+ time: Long = this.eventTime,
+ gestureStart: Long = this.downTime
+) =
+ obtainMotionEvent(
+ action,
+ pointerProperties.toTypedArray(),
+ pointerCoords.toTypedArray(),
+ time,
+ gestureStart
+ )
+
private fun Rect.scaled(scaleFactor: Float) = Rect(this).apply { scale(scaleFactor) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
index 262b4b8..80c3e5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -69,6 +70,8 @@
lateinit var cameraIntents: CameraIntentsWrapper
@Mock
lateinit var contentResolver: ContentResolver
+ @Mock
+ lateinit var userTracker: UserTracker
private lateinit var underTest: CameraGestureHelper
@@ -96,6 +99,7 @@
cameraIntents = cameraIntents,
contentResolver = contentResolver,
uiExecutor = MoreExecutors.directExecutor(),
+ userTracker = userTracker,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
index d159714..d6cafcb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
@@ -16,18 +16,23 @@
package com.android.systemui.charging
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
+import android.view.Surface
import android.view.View
import android.view.WindowManager
+import android.view.WindowMetrics
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.surfaceeffects.ripple.RippleView
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.surfaceeffects.ripple.RippleView
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
import org.junit.Test
@@ -35,12 +40,12 @@
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Mock
+import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -54,6 +59,7 @@
@Mock private lateinit var rippleView: RippleView
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var uiEventLogger: UiEventLogger
+ @Mock private lateinit var windowMetrics: WindowMetrics
private val systemClock = FakeSystemClock()
@Before
@@ -66,6 +72,9 @@
rippleView.setupShader()
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
controller.registerCallbacks()
+
+ `when`(windowMetrics.bounds).thenReturn(Rect(0, 0, 100, 100))
+ `when`(windowManager.currentWindowMetrics).thenReturn(windowMetrics)
}
@Test
@@ -164,4 +173,63 @@
verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture())
verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>())
}
+
+ @Test
+ fun testRipple_layoutsCorrectly() {
+ // Sets the correct ripple size.
+ val width = 100
+ val height = 200
+ whenever(windowMetrics.bounds).thenReturn(Rect(0, 0, width, height))
+
+ // Trigger ripple.
+ val captor = ArgumentCaptor
+ .forClass(BatteryController.BatteryStateChangeCallback::class.java)
+ verify(batteryController).addCallback(captor.capture())
+
+ captor.value.onBatteryLevelChanged(
+ /* unusedBatteryLevel= */ 0,
+ /* plugged in= */ true,
+ /* charging= */ false)
+
+ val attachListenerCaptor =
+ ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
+ verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
+ verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>())
+
+ val runnableCaptor =
+ ArgumentCaptor.forClass(Runnable::class.java)
+ attachListenerCaptor.value.onViewAttachedToWindow(rippleView)
+ verify(rippleView).startRipple(runnableCaptor.capture())
+
+ // Verify size and center position.
+ val maxSize = 400f // Double the max value between width and height.
+ verify(rippleView).setMaxSize(maxWidth = maxSize, maxHeight = maxSize)
+
+ val normalizedPortPosX =
+ context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_x)
+ val normalizedPortPosY =
+ context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_y)
+ val expectedCenterX: Float
+ val expectedCenterY: Float
+ when (context.display.rotation) {
+ Surface.ROTATION_90 -> {
+ expectedCenterX = width * normalizedPortPosY
+ expectedCenterY = height * (1 - normalizedPortPosX)
+ }
+ Surface.ROTATION_180 -> {
+ expectedCenterX = width * (1 - normalizedPortPosX)
+ expectedCenterY = height * (1 - normalizedPortPosY)
+ }
+ Surface.ROTATION_270 -> {
+ expectedCenterX = width * (1 - normalizedPortPosY)
+ expectedCenterY = height * normalizedPortPosX
+ }
+ else -> { // Surface.ROTATION_0
+ expectedCenterX = width * normalizedPortPosX
+ expectedCenterY = height * normalizedPortPosY
+ }
+ }
+
+ verify(rippleView).setCenter(expectedCenterX, expectedCenterY)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
index 0fadc13..e4df754 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
@@ -106,6 +106,7 @@
mClassifiers.add(mClassifierB);
when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mFalsingDataProvider.isFolded()).thenReturn(true);
mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider,
mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier,
mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController,
@@ -121,6 +122,7 @@
mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue();
mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true);
mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true);
+ mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
index 4281ee0..ae38eb6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
@@ -89,25 +89,27 @@
mClassifiers.add(mClassifierA);
when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mFalsingDataProvider.isFolded()).thenReturn(true);
mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider,
mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier,
mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController,
mAccessibilityManager, false, mFakeFeatureFlags);
mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true);
+ mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true);
}
@Test
public void testA11yDisablesGesture() {
- assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue();
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
- assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
}
@Test
public void testA11yDisablesTap() {
- assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
+ assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue();
when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
- assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
+ assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
}
@@ -179,4 +181,11 @@
when(mFalsingDataProvider.isA11yAction()).thenReturn(true);
assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
}
+
+ @Test
+ public void testSkipUnfolded() {
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
+ when(mFalsingDataProvider.isFolded()).thenReturn(false);
+ assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
index 5fa7214..94cf384 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.classifier;
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
@@ -38,6 +39,7 @@
private float mOffsetY = 0;
@Mock
private BatteryController mBatteryController;
+ private FoldStateListener mFoldStateListener = new FoldStateListener(mContext);
private final DockManagerFake mDockManager = new DockManagerFake();
public void setup() {
@@ -47,7 +49,8 @@
displayMetrics.ydpi = 100;
displayMetrics.widthPixels = 1000;
displayMetrics.heightPixels = 1000;
- mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager);
+ mDataProvider = new FalsingDataProvider(
+ displayMetrics, mBatteryController, mFoldStateListener, mDockManager);
}
@After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
index d315c2d..c451a1e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
@@ -24,6 +24,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
@@ -50,6 +51,8 @@
private FalsingDataProvider mDataProvider;
@Mock
private BatteryController mBatteryController;
+ @Mock
+ private FoldStateListener mFoldStateListener;
private final DockManagerFake mDockManager = new DockManagerFake();
@Before
@@ -61,7 +64,8 @@
displayMetrics.ydpi = 100;
displayMetrics.widthPixels = 1000;
displayMetrics.heightPixels = 1000;
- mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager);
+ mDataProvider = new FalsingDataProvider(
+ displayMetrics, mBatteryController, mFoldStateListener, mDockManager);
}
@After
@@ -316,4 +320,16 @@
mDataProvider.onA11yAction();
assertThat(mDataProvider.isA11yAction()).isTrue();
}
+
+ @Test
+ public void test_FoldedState_Folded() {
+ when(mFoldStateListener.getFolded()).thenReturn(true);
+ assertThat(mDataProvider.isFolded()).isTrue();
+ }
+
+ @Test
+ public void test_FoldedState_Unfolded() {
+ when(mFoldStateListener.getFolded()).thenReturn(false);
+ assertThat(mDataProvider.isFolded()).isFalse();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index bdd496e..71c335e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -16,8 +16,6 @@
package com.android.systemui.clipboardoverlay;
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
-
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
import static org.junit.Assert.assertEquals;
@@ -33,7 +31,6 @@
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.os.PersistableBundle;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import androidx.test.filters.SmallTest;
@@ -41,9 +38,6 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.util.DeviceConfigProxyFake;
import org.junit.Before;
import org.junit.Test;
@@ -63,18 +57,11 @@
@Mock
private ClipboardManager mClipboardManager;
@Mock
- private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory;
- @Mock
- private ClipboardOverlayControllerLegacy mOverlayControllerLegacy;
- @Mock
private ClipboardOverlayController mOverlayController;
@Mock
private ClipboardToast mClipboardToast;
@Mock
private UiEventLogger mUiEventLogger;
- @Mock
- private FeatureFlags mFeatureFlags;
- private DeviceConfigProxyFake mDeviceConfigProxy;
private ClipData mSampleClipData;
private String mSampleSource = "Example source";
@@ -97,8 +84,6 @@
mOverlayControllerProvider = () -> mOverlayController;
MockitoAnnotations.initMocks(this);
- when(mClipboardOverlayControllerLegacyFactory.create(any()))
- .thenReturn(mOverlayControllerLegacy);
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
Settings.Secure.putInt(
mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1);
@@ -108,26 +93,13 @@
when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
- mDeviceConfigProxy = new DeviceConfigProxyFake();
-
- mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardToast, mClipboardManager, mUiEventLogger, mFeatureFlags);
+ mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
+ mClipboardToast, mClipboardManager, mUiEventLogger);
}
- @Test
- public void test_disabled() {
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
- "false", false);
- mClipboardListener.start();
- verifyZeroInteractions(mClipboardManager);
- verifyZeroInteractions(mUiEventLogger);
- }
@Test
- public void test_enabled() {
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
- "true", false);
+ public void test_initialization() {
mClipboardListener.start();
verify(mClipboardManager).addPrimaryClipChangedListener(any());
verifyZeroInteractions(mUiEventLogger);
@@ -135,45 +107,6 @@
@Test
public void test_consecutiveCopies() {
- when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
-
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
- "true", false);
- mClipboardListener.start();
- mClipboardListener.onPrimaryClipChanged();
-
- verify(mClipboardOverlayControllerLegacyFactory).create(any());
-
- verify(mOverlayControllerLegacy).setClipData(
- mClipDataCaptor.capture(), mStringCaptor.capture());
-
- assertEquals(mSampleClipData, mClipDataCaptor.getValue());
- assertEquals(mSampleSource, mStringCaptor.getValue());
-
- verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture());
-
- // Should clear the overlay controller
- mRunnableCaptor.getValue().run();
-
- mClipboardListener.onPrimaryClipChanged();
-
- verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
-
- // Not calling the runnable here, just change the clip again and verify that the overlay is
- // NOT recreated.
-
- mClipboardListener.onPrimaryClipChanged();
-
- verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
- verifyZeroInteractions(mOverlayControllerProvider);
- }
-
- @Test
- public void test_consecutiveCopies_new() {
- when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
-
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
- "true", false);
mClipboardListener.start();
mClipboardListener.onPrimaryClipChanged();
@@ -200,7 +133,6 @@
mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider, times(2)).get();
- verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
}
@Test
@@ -231,23 +163,6 @@
@Test
public void test_logging_enterAndReenter() {
- when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
-
- mClipboardListener.start();
-
- mClipboardListener.onPrimaryClipChanged();
- mClipboardListener.onPrimaryClipChanged();
-
- verify(mUiEventLogger, times(1)).log(
- ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
- verify(mUiEventLogger, times(1)).log(
- ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
- }
-
- @Test
- public void test_logging_enterAndReenter_new() {
- when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
-
mClipboardListener.start();
mClipboardListener.onPrimaryClipChanged();
@@ -271,6 +186,5 @@
ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource);
verify(mClipboardToast, times(1)).showCopiedToast();
verifyZeroInteractions(mOverlayControllerProvider);
- verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index b4e85c0..ca5b7af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -47,6 +47,7 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.screenshot.TimeoutHandler;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -81,6 +82,7 @@
private ClipboardOverlayUtils mClipboardUtils;
@Mock
private UiEventLogger mUiEventLogger;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
@@ -116,7 +118,8 @@
mFeatureFlags,
mClipboardUtils,
mExecutor,
- mUiEventLogger);
+ mUiEventLogger,
+ mDisplayTracker);
verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
mCallbacks = mOverlayCallbacksCaptor.getValue();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
new file mode 100644
index 0000000..fe352fd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.common.ui.view
+
+import android.view.ViewConfiguration
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Down
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Move
+import com.android.systemui.common.ui.view.LongPressHandlingViewInteractionHandler.MotionEventModel.Up
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class LongPressHandlingViewInteractionHandlerTest : SysuiTestCase() {
+
+ @Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
+ @Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
+ @Mock private lateinit var onSingleTapDetected: () -> Unit
+
+ private lateinit var underTest: LongPressHandlingViewInteractionHandler
+
+ private var isAttachedToWindow: Boolean = true
+ private var delayedRunnable: Runnable? = null
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(postDelayed.invoke(any(), any())).thenAnswer { invocation ->
+ delayedRunnable = invocation.arguments[0] as Runnable
+ DisposableHandle { delayedRunnable = null }
+ }
+
+ underTest =
+ LongPressHandlingViewInteractionHandler(
+ postDelayed = postDelayed,
+ isAttachedToWindow = { isAttachedToWindow },
+ onLongPressDetected = onLongPressDetected,
+ onSingleTapDetected = onSingleTapDetected,
+ )
+ underTest.isLongPressHandlingEnabled = true
+ }
+
+ @Test
+ fun `long-press`() = runTest {
+ val downX = 123
+ val downY = 456
+ dispatchTouchEvents(
+ Down(
+ x = downX,
+ y = downY,
+ ),
+ Move(
+ distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onLongPressDetected).invoke(downX, downY)
+ verify(onSingleTapDetected, never()).invoke()
+ }
+
+ @Test
+ fun `long-press but feature not enabled`() = runTest {
+ underTest.isLongPressHandlingEnabled = false
+ dispatchTouchEvents(
+ Down(
+ x = 123,
+ y = 456,
+ ),
+ )
+
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected, never()).invoke()
+ }
+
+ @Test
+ fun `long-press but view not attached`() = runTest {
+ isAttachedToWindow = false
+ dispatchTouchEvents(
+ Down(
+ x = 123,
+ y = 456,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected, never()).invoke()
+ }
+
+ @Test
+ fun `dragged too far to be considered a long-press`() = runTest {
+ dispatchTouchEvents(
+ Down(
+ x = 123,
+ y = 456,
+ ),
+ Move(
+ distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f,
+ ),
+ )
+
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected, never()).invoke()
+ }
+
+ @Test
+ fun `held down too briefly to be considered a long-press`() = runTest {
+ dispatchTouchEvents(
+ Down(
+ x = 123,
+ y = 456,
+ ),
+ Up(
+ distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
+ gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
+ ),
+ )
+
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected).invoke()
+ }
+
+ private fun dispatchTouchEvents(
+ vararg models: MotionEventModel,
+ ) {
+ models.forEach { model -> underTest.onTouchEvent(model) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt
new file mode 100644
index 0000000..3e6cc3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.compose
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.ViewUtils
+import android.widget.FrameLayout
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ComposeInitializerTest : SysuiTestCase() {
+ @Test
+ fun testCanAddComposeViewInInitializedWindow() {
+ if (!ComposeFacade.isComposeAvailable()) {
+ return
+ }
+
+ val root = TestWindowRoot(context)
+ try {
+ runOnMainThreadAndWaitForIdleSync { ViewUtils.attachView(root) }
+ assertThat(root.isAttachedToWindow).isTrue()
+
+ runOnMainThreadAndWaitForIdleSync { root.addView(ComposeView(context)) }
+ } finally {
+ runOnMainThreadAndWaitForIdleSync { ViewUtils.detachView(root) }
+ }
+ }
+
+ private fun runOnMainThreadAndWaitForIdleSync(f: () -> Unit) {
+ mContext.mainExecutor.execute(f)
+ waitForIdleSync()
+ }
+
+ class TestWindowRoot(context: Context) : FrameLayout(context) {
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ ComposeFacade.composeInitializer().onAttachedToWindow(this)
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ ComposeFacade.composeInitializer().onDetachedFromWindow(this)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
index 0a81c38..ebbe096 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt
@@ -269,6 +269,14 @@
}
@Test
+ fun testBindServiceForPanel() {
+ controller.bindServiceForPanel(TEST_COMPONENT_NAME_1)
+ executor.runAllReady()
+
+ verify(providers[0]).bindServiceForPanel()
+ }
+
+ @Test
fun testSubscribe() {
val controlInfo1 = ControlInfo("id_1", "", "", DeviceTypes.TYPE_UNKNOWN)
val controlInfo2 = ControlInfo("id_2", "", "", DeviceTypes.TYPE_UNKNOWN)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 1b34706..d54babf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -33,6 +33,7 @@
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserFileManager
@@ -66,6 +67,7 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
+import org.mockito.Mockito.clearInvocations
import org.mockito.MockitoAnnotations
@SmallTest
@@ -88,6 +90,8 @@
private lateinit var userTracker: UserTracker
@Mock
private lateinit var userFileManager: UserFileManager
+ @Mock
+ private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
@Captor
private lateinit var structureInfoCaptor: ArgumentCaptor<StructureInfo>
@@ -168,6 +172,7 @@
listingController,
userFileManager,
userTracker,
+ authorizedPanelsRepository,
Optional.of(persistenceWrapper),
mock(DumpManager::class.java)
)
@@ -224,6 +229,7 @@
listingController,
userFileManager,
userTracker,
+ authorizedPanelsRepository,
Optional.of(persistenceWrapper),
mock(DumpManager::class.java)
)
@@ -231,6 +237,26 @@
}
@Test
+ fun testAddAuthorizedPackagesFromSavedFavoritesOnStart() {
+ clearInvocations(authorizedPanelsRepository)
+ `when`(persistenceWrapper.readFavorites()).thenReturn(listOf(TEST_STRUCTURE_INFO))
+ ControlsControllerImpl(
+ mContext,
+ delayableExecutor,
+ uiController,
+ bindingController,
+ listingController,
+ userFileManager,
+ userTracker,
+ authorizedPanelsRepository,
+ Optional.of(persistenceWrapper),
+ mock(DumpManager::class.java)
+ )
+ verify(authorizedPanelsRepository)
+ .addAuthorizedPanels(setOf(TEST_STRUCTURE_INFO.componentName.packageName))
+ }
+
+ @Test
fun testOnActionResponse() {
controller.onActionResponse(TEST_COMPONENT, TEST_CONTROL_ID, ControlAction.RESPONSE_OK)
@@ -919,6 +945,12 @@
.getFile(ControlsFavoritePersistenceWrapper.FILE_NAME, context.user.identifier)
assertThat(userStructure.file).isNotNull()
}
+
+ @Test
+ fun testBindForPanel() {
+ controller.bindComponentForPanel(TEST_COMPONENT)
+ verify(bindingController).bindServiceForPanel(TEST_COMPONENT)
+ }
}
private class DidRunRunnable() : Runnable {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
index af3f24a..da548f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt
@@ -105,6 +105,22 @@
}
@Test
+ fun testBindForPanel() {
+ manager.bindServiceForPanel()
+ executor.runAllReady()
+ assertTrue(context.isBound(componentName))
+ }
+
+ @Test
+ fun testUnbindPanelIsUnbound() {
+ manager.bindServiceForPanel()
+ executor.runAllReady()
+ manager.unbindService()
+ executor.runAllReady()
+ assertFalse(context.isBound(componentName))
+ }
+
+ @Test
fun testNullBinding() {
val mockContext = mock(Context::class.java)
lateinit var serviceConnection: ServiceConnection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
index 765c4c0..226ef3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt
@@ -21,12 +21,15 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.LayoutInflater
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.settingslib.core.lifecycle.Lifecycle
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -36,8 +39,10 @@
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.`when`
+import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import java.text.Collator
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -49,25 +54,18 @@
@Mock lateinit var lifecycle: Lifecycle
@Mock lateinit var controlsListingController: ControlsListingController
@Mock lateinit var layoutInflater: LayoutInflater
- @Mock lateinit var onAppSelected: (ComponentName?) -> Unit
+ @Mock lateinit var onAppSelected: (ControlsServiceInfo) -> Unit
@Mock lateinit var favoritesRenderer: FavoritesRenderer
val resources: Resources = context.resources
lateinit var adapter: AppAdapter
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- adapter = AppAdapter(backgroundExecutor,
- uiExecutor,
- lifecycle,
- controlsListingController,
- layoutInflater,
- onAppSelected,
- favoritesRenderer,
- resources)
}
@Test
fun testOnServicesUpdated_nullLoadLabel() {
+ adapter = createAdapterWithAuthorizedPanels(emptySet())
val captor = ArgumentCaptor
.forClass(ControlsListingController.ControlsListingCallback::class.java)
val controlsServiceInfo = mock<ControlsServiceInfo>()
@@ -76,14 +74,14 @@
verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
captor.value.onServicesUpdated(serviceInfo)
- backgroundExecutor.runAllReady()
- uiExecutor.runAllReady()
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
assertThat(adapter.itemCount).isEqualTo(serviceInfo.size)
}
@Test
- fun testOnServicesUpdatedDoesntHavePanels() {
+ fun testOnServicesUpdated_showsNotAuthorizedPanels() {
+ adapter = createAdapterWithAuthorizedPanels(emptySet())
val captor = ArgumentCaptor
.forClass(ControlsListingController.ControlsListingCallback::class.java)
val serviceInfo = listOf(
@@ -93,20 +91,88 @@
verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
captor.value.onServicesUpdated(serviceInfo)
- backgroundExecutor.runAllReady()
- uiExecutor.runAllReady()
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+
+ assertThat(adapter.itemCount).isEqualTo(2)
+ }
+
+ @Test
+ fun testOnServicesUpdated_doesntShowAuthorizedPanels() {
+ adapter = createAdapterWithAuthorizedPanels(setOf(TEST_PACKAGE))
+
+ val captor = ArgumentCaptor
+ .forClass(ControlsListingController.ControlsListingCallback::class.java)
+ val serviceInfo = listOf(
+ ControlsServiceInfo("no panel", null),
+ ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls"))
+ )
+ verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
+
+ captor.value.onServicesUpdated(serviceInfo)
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
assertThat(adapter.itemCount).isEqualTo(1)
}
- fun ControlsServiceInfo(
- label: CharSequence,
- panelComponentName: ComponentName? = null
- ): ControlsServiceInfo {
- return mock {
- `when`(this.loadLabel()).thenReturn(label)
- `when`(this.panelActivity).thenReturn(panelComponentName)
- `when`(this.loadIcon()).thenReturn(mock())
+ @Test
+ fun testOnBindSetsClickListenerToCallOnAppSelected() {
+ adapter = createAdapterWithAuthorizedPanels(emptySet())
+
+ val captor = ArgumentCaptor
+ .forClass(ControlsListingController.ControlsListingCallback::class.java)
+ val serviceInfo = listOf(
+ ControlsServiceInfo("no panel", null),
+ ControlsServiceInfo("panel", ComponentName(TEST_PACKAGE, "cls"))
+ )
+ verify(controlsListingController).observe(any(Lifecycle::class.java), captor.capture())
+
+ captor.value.onServicesUpdated(serviceInfo)
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+
+ val sorted = serviceInfo.sortedWith(
+ compareBy(Collator.getInstance(resources.configuration.locales[0])) {
+ it.loadLabel() ?: ""
+ })
+
+ sorted.forEachIndexed { index, info ->
+ val fakeView: View = mock()
+ val fakeHolder: AppAdapter.Holder = mock()
+ `when`(fakeHolder.view).thenReturn(fakeView)
+
+ clearInvocations(onAppSelected)
+ adapter.onBindViewHolder(fakeHolder, index)
+ val listenerCaptor: ArgumentCaptor<View.OnClickListener> = argumentCaptor()
+ verify(fakeView).setOnClickListener(capture(listenerCaptor))
+ listenerCaptor.value.onClick(fakeView)
+
+ verify(onAppSelected).invoke(info)
}
}
+
+ private fun createAdapterWithAuthorizedPanels(packages: Set<String>): AppAdapter {
+ return AppAdapter(backgroundExecutor,
+ uiExecutor,
+ lifecycle,
+ controlsListingController,
+ layoutInflater,
+ onAppSelected,
+ favoritesRenderer,
+ resources,
+ packages)
+ }
+
+ companion object {
+ private fun ControlsServiceInfo(
+ label: CharSequence,
+ panelComponentName: ComponentName? = null
+ ): ControlsServiceInfo {
+ return mock {
+ `when`(loadLabel()).thenReturn(label)
+ `when`(panelActivity).thenReturn(panelComponentName)
+ `when`(loadIcon()).thenReturn(mock())
+ }
+ }
+
+ private const val TEST_PACKAGE = "package"
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
index 56c3efe..8dfd223 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
@@ -16,7 +16,13 @@
package com.android.systemui.controls.management
+import android.app.Dialog
+import android.content.ComponentName
import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
+import android.os.Bundle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.window.OnBackInvokedCallback
@@ -25,14 +31,23 @@
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.intercepting.SingleActivityFactory
import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
-import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
+import java.util.function.Consumer
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -41,7 +56,11 @@
import org.mockito.ArgumentMatchers
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
@SmallTest
@@ -58,9 +77,10 @@
@Mock lateinit var userTracker: UserTracker
- @Mock lateinit var uiController: ControlsUiController
+ @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
- private lateinit var controlsProviderSelectorActivity: ControlsProviderSelectorActivity_Factory
+ @Mock lateinit var dialogFactory: PanelConfirmationDialogFactory
+
private var latch: CountDownLatch = CountDownLatch(1)
@Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
@@ -81,7 +101,8 @@
listingController,
controlsController,
userTracker,
- uiController,
+ authorizedPanelsRepository,
+ dialogFactory,
mockDispatcher,
latch
)
@@ -113,13 +134,99 @@
verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value)
}
- public class TestableControlsProviderSelectorActivity(
+ @Test
+ fun testOnAppSelectedForNonPanelStartsFavoritingActivity() {
+ val info = ControlsServiceInfo(ComponentName("test_pkg", "service"), "", null)
+ activityRule.activity.onAppSelected(info)
+
+ verifyNoMoreInteractions(dialogFactory)
+
+ assertThat(activityRule.activity.lastStartedActivity?.component?.className)
+ .isEqualTo(ControlsFavoritingActivity::class.java.name)
+
+ assertThat(activityRule.activity.triedToFinish).isTrue()
+ }
+
+ @Test
+ fun testOnAppSelectedForPanelTriggersDialog() {
+ val label = "label"
+ val info =
+ ControlsServiceInfo(
+ ComponentName("test_pkg", "service"),
+ label,
+ ComponentName("test_pkg", "activity")
+ )
+
+ val dialog: Dialog = mock()
+ whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
+
+ activityRule.activity.onAppSelected(info)
+ verify(dialogFactory).createConfirmationDialog(any(), eq(label), any())
+ verify(dialog).show()
+
+ assertThat(activityRule.activity.triedToFinish).isFalse()
+ }
+
+ @Test
+ fun dialogAcceptAddsPackage() {
+ val label = "label"
+ val info =
+ ControlsServiceInfo(
+ ComponentName("test_pkg", "service"),
+ label,
+ ComponentName("test_pkg", "activity")
+ )
+
+ val dialog: Dialog = mock()
+ whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
+
+ activityRule.activity.onAppSelected(info)
+
+ val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor()
+ verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor))
+
+ captor.value.accept(true)
+
+ val setCaptor: ArgumentCaptor<Set<String>> = argumentCaptor()
+ verify(authorizedPanelsRepository).addAuthorizedPanels(capture(setCaptor))
+ assertThat(setCaptor.value).containsExactly(info.componentName.packageName)
+
+ assertThat(activityRule.activity.triedToFinish).isTrue()
+ }
+
+ @Test
+ fun dialogCancelDoesntAddPackage() {
+ val label = "label"
+ val info =
+ ControlsServiceInfo(
+ ComponentName("test_pkg", "service"),
+ label,
+ ComponentName("test_pkg", "activity")
+ )
+
+ val dialog: Dialog = mock()
+ whenever(dialogFactory.createConfirmationDialog(any(), any(), any())).thenReturn(dialog)
+
+ activityRule.activity.onAppSelected(info)
+
+ val captor: ArgumentCaptor<Consumer<Boolean>> = argumentCaptor()
+ verify(dialogFactory).createConfirmationDialog(any(), any(), capture(captor))
+
+ captor.value.accept(false)
+
+ verify(authorizedPanelsRepository, never()).addAuthorizedPanels(any())
+
+ assertThat(activityRule.activity.triedToFinish).isFalse()
+ }
+
+ class TestableControlsProviderSelectorActivity(
executor: Executor,
backExecutor: Executor,
listingController: ControlsListingController,
controlsController: ControlsController,
userTracker: UserTracker,
- uiController: ControlsUiController,
+ authorizedPanelsRepository: AuthorizedPanelsRepository,
+ dialogFactory: PanelConfirmationDialogFactory,
private val mockDispatcher: OnBackInvokedDispatcher,
private val latch: CountDownLatch
) :
@@ -129,16 +236,50 @@
listingController,
controlsController,
userTracker,
- uiController
+ authorizedPanelsRepository,
+ dialogFactory
) {
+
+ var lastStartedActivity: Intent? = null
+ var triedToFinish = false
+
override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
return mockDispatcher
}
+ override fun startActivity(intent: Intent?, options: Bundle?) {
+ lastStartedActivity = intent
+ }
+
override fun onStop() {
super.onStop()
// ensures that test runner thread does not proceed until ui thread is done
latch.countDown()
}
+
+ override fun animateExitAndFinish() {
+ // Activity should only be finished from the rule.
+ triedToFinish = true
+ }
+ }
+
+ companion object {
+ private fun ControlsServiceInfo(
+ componentName: ComponentName,
+ label: CharSequence,
+ panelComponentName: ComponentName? = null
+ ): ControlsServiceInfo {
+ val serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo()
+ packageName = componentName.packageName
+ name = componentName.className
+ }
+ return Mockito.spy(ControlsServiceInfo(mock(), serviceInfo)).apply {
+ doReturn(label).`when`(this).loadLabel()
+ doReturn(mock<Drawable>()).`when`(this).loadIcon()
+ doReturn(panelComponentName).`when`(this).panelActivity
+ }
+ }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
new file mode 100644
index 0000000..756f267
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.controls.management
+
+import android.content.DialogInterface
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PanelConfirmationDialogFactoryTest : SysuiTestCase() {
+
+ @Test
+ fun testDialogHasCorrectInfo() {
+ val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+ val factory = PanelConfirmationDialogFactory { mockDialog }
+ val appName = "appName"
+
+ factory.createConfirmationDialog(context, appName) {}
+
+ verify(mockDialog).setCanceledOnTouchOutside(true)
+ verify(mockDialog)
+ .setTitle(context.getString(R.string.controls_panel_authorization_title, appName))
+ verify(mockDialog)
+ .setMessage(context.getString(R.string.controls_panel_authorization, appName))
+ }
+
+ @Test
+ fun testDialogPositiveButton() {
+ val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+ val factory = PanelConfirmationDialogFactory { mockDialog }
+
+ var response: Boolean? = null
+
+ factory.createConfirmationDialog(context, "") { response = it }
+
+ val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
+ verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor))
+
+ captor.value.onClick(mockDialog, DialogInterface.BUTTON_POSITIVE)
+
+ assertThat(response).isTrue()
+ }
+
+ @Test
+ fun testDialogNeutralButton() {
+ val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+ val factory = PanelConfirmationDialogFactory { mockDialog }
+
+ var response: Boolean? = null
+
+ factory.createConfirmationDialog(context, "") { response = it }
+
+ val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
+ verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor))
+
+ captor.value.onClick(mockDialog, DialogInterface.BUTTON_NEUTRAL)
+
+ assertThat(response).isFalse()
+ }
+
+ @Test
+ fun testDialogCancel() {
+ val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
+ val factory = PanelConfirmationDialogFactory { mockDialog }
+
+ var response: Boolean? = null
+
+ factory.createConfirmationDialog(context, "") { response = it }
+
+ val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor()
+ verify(mockDialog).setOnCancelListener(capture(captor))
+
+ captor.value.onCancel(mockDialog)
+
+ assertThat(response).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
new file mode 100644
index 0000000..b91a3fd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.controls.panels
+
+import android.content.SharedPreferences
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() {
+
+ @Mock private lateinit var userTracker: UserTracker
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mContext.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf<String>()
+ )
+ whenever(userTracker.userId).thenReturn(0)
+ }
+
+ @Test
+ fun testPreApprovedPackagesAreSeededIfNoSavedPreferences() {
+ mContext.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE)
+ )
+ val sharedPrefs = FakeSharedPreferences()
+ val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+ val repository = createRepository(fileManager)
+
+ assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
+ assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE)
+ }
+
+ @Test
+ fun testPreApprovedPackagesNotSeededIfEmptySavedPreferences() {
+ mContext.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE)
+ )
+ val sharedPrefs = FakeSharedPreferences()
+ sharedPrefs.edit().putStringSet(KEY, emptySet()).apply()
+ val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+ createRepository(fileManager)
+
+ assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty()
+ }
+
+ @Test
+ fun testPreApprovedPackagesOnlySetForUserThatDoesntHaveThem() {
+ mContext.orCreateTestableResources.addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(TEST_PACKAGE)
+ )
+ val sharedPrefs_0 = FakeSharedPreferences()
+ val sharedPrefs_1 = FakeSharedPreferences()
+ sharedPrefs_1.edit().putStringSet(KEY, emptySet()).apply()
+ val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs_0, 1 to sharedPrefs_1))
+ val repository = createRepository(fileManager)
+
+ assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
+ whenever(userTracker.userId).thenReturn(1)
+ assertThat(repository.getAuthorizedPanels()).isEmpty()
+ }
+
+ @Test
+ fun testGetAuthorizedPackages() {
+ val sharedPrefs = FakeSharedPreferences()
+ sharedPrefs.edit().putStringSet(KEY, mutableSetOf(TEST_PACKAGE)).apply()
+ val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+
+ val repository = createRepository(fileManager)
+ assertThat(repository.getAuthorizedPanels()).containsExactly(TEST_PACKAGE)
+ }
+
+ @Test
+ fun testSetAuthorizedPackage() {
+ val sharedPrefs = FakeSharedPreferences()
+ val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs))
+
+ val repository = createRepository(fileManager)
+ repository.addAuthorizedPanels(setOf(TEST_PACKAGE))
+ assertThat(sharedPrefs.getStringSet(KEY, null)).containsExactly(TEST_PACKAGE)
+ }
+
+ private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl {
+ return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker)
+ }
+
+ private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) :
+ UserFileManager {
+ override fun getFile(fileName: String, userId: Int): File {
+ throw UnsupportedOperationException()
+ }
+
+ override fun getSharedPreferences(
+ fileName: String,
+ mode: Int,
+ userId: Int
+ ): SharedPreferences {
+ if (fileName != FILE_NAME) {
+ throw IllegalArgumentException("Preference files must be $FILE_NAME")
+ }
+ return sharedPrefs.getValue(userId)
+ }
+ }
+
+ companion object {
+ private const val FILE_NAME = "controls_prefs"
+ private const val KEY = "authorized_panels"
+ private const val TEST_PACKAGE = "package"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index d172c9a..85f9961 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -20,6 +20,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.service.controls.ControlsProviderService
@@ -39,8 +40,10 @@
import com.android.systemui.controls.controller.StructureInfo
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.management.ControlsProviderSelectorActivity
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
import com.android.systemui.controls.settings.FakeControlsSettingsRepository
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
@@ -91,6 +94,9 @@
@Mock lateinit var userTracker: UserTracker
@Mock lateinit var taskViewFactory: TaskViewFactory
@Mock lateinit var dumpManager: DumpManager
+ @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+ @Mock lateinit var featureFlags: FeatureFlags
+ @Mock lateinit var packageManager: PackageManager
val sharedPreferences = FakeSharedPreferences()
lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
@@ -120,6 +126,7 @@
ControlsUiControllerImpl(
Lazy { controlsController },
context,
+ packageManager,
uiExecutor,
bgExecutor,
Lazy { controlsListingController },
@@ -132,6 +139,8 @@
userTracker,
Optional.of(taskViewFactory),
controlsSettingsRepository,
+ authorizedPanelsRepository,
+ featureFlags,
dumpManager
)
`when`(
@@ -229,9 +238,20 @@
}
@Test
+ fun testPanelBindsForPanel() {
+ val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ setUpPanel(panel)
+
+ underTest.show(parent, {}, context)
+ verify(controlsController).bindComponentForPanel(panel.componentName)
+ }
+
+ @Test
fun testPanelCallsTaskViewFactoryCreate() {
mockLayoutInflater()
- val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ val packageName = "pkg"
+ `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
+ val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls"))
val serviceInfo = setUpPanel(panel)
underTest.show(parent, {}, context)
@@ -249,9 +269,11 @@
@Test
fun testPanelControllerStartActivityWithCorrectArguments() {
mockLayoutInflater()
+ val packageName = "pkg"
+ `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
- val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls"))
val serviceInfo = setUpPanel(panel)
underTest.show(parent, {}, context)
@@ -281,9 +303,11 @@
@Test
fun testPendingIntentExtrasAreModified() {
mockLayoutInflater()
+ val packageName = "pkg"
+ `when`(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf(packageName))
controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
- val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls"))
+ val panel = SelectedItem.PanelItem("App name", ComponentName(packageName, "cls"))
val serviceInfo = setUpPanel(panel)
underTest.show(parent, {}, context)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt
new file mode 100644
index 0000000..dbaf94f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.controls.ui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class OverflowMenuAdapterTest : SysuiTestCase() {
+
+ @Test
+ fun testGetItemId() {
+ val ids = listOf(27L, 73L)
+ val labels = listOf("first", "second")
+ val adapter =
+ OverflowMenuAdapter(
+ context,
+ layoutId = 0,
+ labels.zip(ids).map { OverflowMenuAdapter.MenuItem(it.first, it.second) }
+ ) { true }
+
+ ids.forEachIndexed { index, id -> assertThat(adapter.getItemId(index)).isEqualTo(id) }
+ }
+
+ @Test
+ fun testCheckEnabled() {
+ val ids = listOf(27L, 73L)
+ val labels = listOf("first", "second")
+ val adapter =
+ OverflowMenuAdapter(
+ context,
+ layoutId = 0,
+ labels.zip(ids).map { OverflowMenuAdapter.MenuItem(it.first, it.second) }
+ ) { position -> position == 0 }
+
+ assertThat(adapter.isEnabled(0)).isTrue()
+ assertThat(adapter.isEnabled(1)).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
index 5c2b153..af027e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeDockHandlerTest.java
@@ -26,6 +26,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.hardware.display.AmbientDisplayConfiguration;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
@@ -36,6 +37,7 @@
import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerFake;
import com.android.systemui.doze.DozeMachine.State;
+import com.android.systemui.settings.UserTracker;
import org.junit.Before;
import org.junit.Test;
@@ -48,6 +50,7 @@
@RunWithLooper
public class DozeDockHandlerTest extends SysuiTestCase {
@Mock private DozeMachine mMachine;
+ @Mock private UserTracker mUserTracker;
private AmbientDisplayConfiguration mConfig;
private DockManagerFake mDockManagerFake;
private DozeDockHandler mDockHandler;
@@ -57,9 +60,10 @@
MockitoAnnotations.initMocks(this);
mConfig = DozeConfigurationUtil.createMockConfig();
mDockManagerFake = spy(new DockManagerFake());
- mDockHandler = new DozeDockHandler(mConfig, mDockManagerFake);
+ mDockHandler = new DozeDockHandler(mConfig, mDockManagerFake, mUserTracker);
mDockHandler.setDozeMachine(mMachine);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mMachine.getState()).thenReturn(State.DOZE_AOD);
doReturn(true).when(mConfig).alwaysOnEnabled(anyInt());
mDockHandler.transitionTo(DozeMachine.State.UNINITIALIZED, DozeMachine.State.INITIALIZED);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 5bbd810..a636b7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -45,6 +45,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.content.res.Configuration;
import android.hardware.display.AmbientDisplayConfiguration;
import android.testing.AndroidTestingRunner;
@@ -57,6 +58,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.util.wakelock.WakeLockFake;
@@ -85,6 +87,8 @@
private DozeMachine.Part mPartMock;
@Mock
private DozeMachine.Part mAnotherPartMock;
+ @Mock
+ private UserTracker mUserTracker;
private DozeServiceFake mServiceFake;
private WakeLockFake mWakeLockFake;
private AmbientDisplayConfiguration mAmbientDisplayConfigMock;
@@ -97,6 +101,7 @@
mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class);
when(mDockManager.isDocked()).thenReturn(false);
when(mDockManager.isHidden()).thenReturn(false);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
mMachine = new DozeMachine(mServiceFake,
mAmbientDisplayConfigMock,
@@ -105,7 +110,8 @@
mDozeLog,
mDockManager,
mHost,
- new DozeMachine.Part[]{mPartMock, mAnotherPartMock});
+ new DozeMachine.Part[]{mPartMock, mAnotherPartMock},
+ mUserTracker);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
index 03827da..3af444a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeScreenBrightnessTest.java
@@ -34,6 +34,7 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.reset;
@@ -57,6 +58,7 @@
import com.android.systemui.util.concurrency.FakeThreadFactory;
import com.android.systemui.util.sensors.AsyncSensorManager;
import com.android.systemui.util.sensors.FakeSensorManager;
+import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
@@ -94,6 +96,8 @@
DevicePostureController mDevicePostureController;
@Mock
DozeLog mDozeLog;
+ @Mock
+ SystemSettings mSystemSettings;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private FakeThreadFactory mFakeThreadFactory = new FakeThreadFactory(mFakeExecutor);
@@ -102,9 +106,8 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- Settings.System.putIntForUser(mContext.getContentResolver(),
- Settings.System.SCREEN_BRIGHTNESS, DEFAULT_BRIGHTNESS,
- UserHandle.USER_CURRENT);
+ when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
+ eq(UserHandle.USER_CURRENT))).thenReturn(DEFAULT_BRIGHTNESS);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
@@ -131,7 +134,8 @@
mWakefulnessLifecycle,
mDozeParameters,
mDevicePostureController,
- mDozeLog);
+ mDozeLog,
+ mSystemSettings);
}
@Test
@@ -157,11 +161,10 @@
}
@Test
- public void testAod_usesLightSensorRespectingUserSetting() throws Exception {
+ public void testAod_usesLightSensorRespectingUserSetting() {
int maxBrightness = 3;
- Settings.System.putIntForUser(mContext.getContentResolver(),
- Settings.System.SCREEN_BRIGHTNESS, maxBrightness,
- UserHandle.USER_CURRENT);
+ when(mSystemSettings.getIntForUser(eq(Settings.System.SCREEN_BRIGHTNESS), anyInt(),
+ eq(UserHandle.USER_CURRENT))).thenReturn(maxBrightness);
mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
assertEquals(maxBrightness, mServiceFake.screenBrightness);
@@ -238,7 +241,8 @@
mWakefulnessLifecycle,
mDozeParameters,
mDevicePostureController,
- mDozeLog);
+ mDozeLog,
+ mSystemSettings);
mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
mScreen.transitionTo(INITIALIZED, DOZE);
reset(mDozeHost);
@@ -275,7 +279,8 @@
mWakefulnessLifecycle,
mDozeParameters,
mDevicePostureController,
- mDozeLog);
+ mDozeLog,
+ mSystemSettings);
mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
mScreen.transitionTo(INITIALIZED, DOZE_AOD);
@@ -306,7 +311,8 @@
mWakefulnessLifecycle,
mDozeParameters,
mDevicePostureController,
- mDozeLog);
+ mDozeLog,
+ mSystemSettings);
// GIVEN the device is in AOD
mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
@@ -344,7 +350,8 @@
mWakefulnessLifecycle,
mDozeParameters,
mDevicePostureController,
- mDozeLog);
+ mDozeLog,
+ mSystemSettings);
// GIVEN device is in AOD
mScreen.transitionTo(UNINITIALIZED, INITIALIZED);
@@ -386,7 +393,8 @@
mWakefulnessLifecycle,
mDozeParameters,
mDevicePostureController,
- mDozeLog);
+ mDozeLog,
+ mSystemSettings);
verify(mDevicePostureController).addCallback(postureCallbackCaptor.capture());
// GIVEN device is in AOD
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index d910a27..986d6d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -35,11 +35,11 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.display.AmbientDisplayConfiguration;
-import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
@@ -120,11 +120,13 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
mTestableLooper = TestableLooper.get(this);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mAmbientDisplayConfiguration.tapSensorTypeMapping())
.thenReturn(new String[]{"tapSensor"});
when(mAmbientDisplayConfiguration.getWakeLockScreenDebounce()).thenReturn(5000L);
when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true);
- when(mAmbientDisplayConfiguration.enabled(UserHandle.USER_CURRENT)).thenReturn(true);
+ when(mAmbientDisplayConfiguration.enabled(ActivityManager.getCurrentUser())).thenReturn(
+ true);
doAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
@@ -144,7 +146,7 @@
@Test
public void testSensorDebounce() {
- mDozeSensors.setListening(true, true);
+ mDozeSensors.setListening(true, true, true);
mWakeLockScreenListener.onSensorChanged(mock(SensorManagerPlugin.SensorEvent.class));
mTestableLooper.processAllMessages();
@@ -162,7 +164,7 @@
@Test
public void testSetListening_firstTrue_registerSettingsObserver() {
verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt());
- mDozeSensors.setListening(true, true);
+ mDozeSensors.setListening(true, true, true);
verify(mTriggerSensor).registerSettingsObserver(any(ContentObserver.class));
}
@@ -170,8 +172,8 @@
@Test
public void testSetListening_twiceTrue_onlyRegisterSettingsObserverOnce() {
verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt());
- mDozeSensors.setListening(true, true);
- mDozeSensors.setListening(true, true);
+ mDozeSensors.setListening(true, true, true);
+ mDozeSensors.setListening(true, true, true);
verify(mTriggerSensor, times(1)).registerSettingsObserver(any(ContentObserver.class));
}
@@ -196,7 +198,7 @@
assertFalse(mSensorTap.mRequested);
// WHEN we're now in a low powered state
- dozeSensors.setListening(true, true, true);
+ dozeSensors.setListeningWithPowerState(true, true, true, true);
// THEN the tap sensor is registered
assertTrue(mSensorTap.mRequested);
@@ -207,12 +209,12 @@
// GIVEN doze sensors enabled
when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
- // GIVEN a trigger sensor
+ // GIVEN a trigger sensor that's enabled by settings
Sensor mockSensor = mock(Sensor.class);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
mockSensor,
- /* settingEnabled */ true,
- /* requiresTouchScreen */ true);
+ /* settingEnabled */ true
+ );
when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
.thenReturn(true);
@@ -228,12 +230,12 @@
// GIVEN doze sensors enabled
when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
- // GIVEN a trigger sensor
+ // GIVEN a trigger sensor that's not enabled by settings
Sensor mockSensor = mock(Sensor.class);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
mockSensor,
- /* settingEnabled*/ false,
- /* requiresTouchScreen */ true);
+ /* settingEnabled*/ false
+ );
when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
.thenReturn(true);
@@ -249,12 +251,12 @@
// GIVEN doze sensors enabled
when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
- // GIVEN a trigger sensor that's
+ // GIVEN a trigger sensor that's not enabled by settings
Sensor mockSensor = mock(Sensor.class);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
mockSensor,
- /* settingEnabled*/ false,
- /* requiresTouchScreen */ true);
+ /* settingEnabled*/ false
+ );
when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
.thenReturn(true);
@@ -264,7 +266,7 @@
// WHEN ignoreSetting is called
triggerSensor.ignoreSetting(true);
- // THEN the sensor is registered
+ // THEN the sensor is still registered since the setting is ignore
assertTrue(triggerSensor.mRegistered);
}
@@ -275,10 +277,10 @@
// GIVEN a trigger sensor
Sensor mockSensor = mock(Sensor.class);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
mockSensor,
- /* settingEnabled*/ true,
- /* requiresTouchScreen */ true);
+ /* settingEnabled*/ true
+ );
when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
.thenReturn(true);
@@ -295,7 +297,7 @@
// GIVEN doze sensor that supports postures
Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
new Sensor[] {
null /* unknown */,
closedSensor,
@@ -316,7 +318,7 @@
// GIVEN doze sensor that supports postures
Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
new Sensor[] {
null /* unknown */,
closedSensor,
@@ -345,7 +347,7 @@
// GIVEN doze sensor that supports postures
Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
new Sensor[] {
null /* unknown */,
closedSensor,
@@ -400,7 +402,7 @@
public void testUdfpsEnrollmentChanged() throws Exception {
// GIVEN a UDFPS_LONG_PRESS trigger sensor that's not configured
Sensor mockSensor = mock(Sensor.class);
- TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+ TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
mockSensor,
REASON_SENSOR_UDFPS_LONG_PRESS,
/* configured */ false);
@@ -409,7 +411,7 @@
.thenReturn(true);
// WHEN listening state is set to TRUE
- mDozeSensors.setListening(true, true);
+ mDozeSensors.setListening(true, true, true);
// THEN mRegistered is still false b/c !mConfigured
assertFalse(triggerSensor.mConfigured);
@@ -439,6 +441,35 @@
}
@Test
+ public void aodOnlySensor_onlyRegisteredWhenAodSensorsIncluded() {
+ // GIVEN doze sensors enabled
+ when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
+
+ // GIVEN a trigger sensor that requires aod
+ Sensor mockSensor = mock(Sensor.class);
+ TriggerSensor aodOnlyTriggerSensor = mDozeSensors.createDozeSensorRequiringAod(mockSensor);
+ when(mSensorManager.requestTriggerSensor(eq(aodOnlyTriggerSensor), eq(mockSensor)))
+ .thenReturn(true);
+ mDozeSensors.addSensor(aodOnlyTriggerSensor);
+
+ // WHEN aod only sensors aren't included
+ mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true,
+ /* includeAodOnlySensors */false);
+
+ // THEN the sensor is not registered or requested
+ assertFalse(aodOnlyTriggerSensor.mRequested);
+ assertFalse(aodOnlyTriggerSensor.mRegistered);
+
+ // WHEN aod only sensors ARE included
+ mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true,
+ /* includeAodOnlySensors */true);
+
+ // THEN the sensor is registered and requested
+ assertTrue(aodOnlyTriggerSensor.mRequested);
+ assertTrue(aodOnlyTriggerSensor.mRegistered);
+ }
+
+ @Test
public void liftToWake_defaultSetting_configDefaultFalse() {
// WHEN the default lift to wake gesture setting is false
when(mResources.getBoolean(
@@ -494,8 +525,8 @@
mTriggerSensors = new TriggerSensor[] {mTriggerSensor, mSensorTap};
}
- public TriggerSensor createDozeSensor(Sensor sensor, boolean settingEnabled,
- boolean requiresTouchScreen) {
+ public TriggerSensor createDozeSensorWithSettingEnabled(Sensor sensor,
+ boolean settingEnabled) {
return new TriggerSensor(/* sensor */ sensor,
/* setting name */ "test_setting",
/* settingDefault */ settingEnabled,
@@ -504,11 +535,13 @@
/* reportsTouchCoordinate*/ false,
/* requiresTouchscreen */ false,
/* ignoresSetting */ false,
- requiresTouchScreen,
- /* immediatelyReRegister */ true);
+ /* requiresProx */ false,
+ /* immediatelyReRegister */ true,
+ /* requiresAod */false
+ );
}
- public TriggerSensor createDozeSensor(
+ public TriggerSensor createDozeSensorForPosture(
Sensor sensor,
int pulseReason,
boolean configured
@@ -522,15 +555,35 @@
/* requiresTouchscreen */ false,
/* ignoresSetting */ false,
/* requiresTouchScreen */ false,
- /* immediatelyReRegister*/ true);
+ /* immediatelyReRegister*/ true,
+ false
+ );
}
/**
- * create a doze sensor that supports postures and is enabled
+ * Create a doze sensor that requires Aod
*/
- public TriggerSensor createDozeSensor(Sensor[] sensors, int posture) {
+ public TriggerSensor createDozeSensorRequiringAod(Sensor sensor) {
+ return new TriggerSensor(/* sensor */ sensor,
+ /* setting name */ "aod_requiring_sensor",
+ /* settingDefault */ true,
+ /* configured */ true,
+ /* pulseReason*/ 0,
+ /* reportsTouchCoordinate*/ false,
+ /* requiresTouchscreen */ false,
+ /* ignoresSetting */ false,
+ /* requiresProx */ false,
+ /* immediatelyReRegister */ true,
+ /* requiresAoD */ true
+ );
+ }
+
+ /**
+ * Create a doze sensor that supports postures and is enabled
+ */
+ public TriggerSensor createDozeSensorForPosture(Sensor[] sensors, int posture) {
return new TriggerSensor(/* sensor */ sensors,
- /* setting name */ "test_setting",
+ /* setting name */ "posture_test_setting",
/* settingDefault */ true,
/* configured */ true,
/* pulseReason*/ 0,
@@ -539,7 +592,9 @@
/* ignoresSetting */ true,
/* requiresProx */ false,
/* immediatelyReRegister */ true,
- posture);
+ posture,
+ /* requiresUi */ false
+ );
}
public void addSensor(TriggerSensor sensor) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index 32b9945..9064470 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -36,6 +36,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.hardware.display.AmbientDisplayConfiguration;
import android.testing.AndroidTestingRunner;
import android.testing.UiThreadTest;
@@ -43,6 +44,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import org.junit.After;
@@ -73,6 +75,8 @@
private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
@Mock
private BiometricUnlockController mBiometricUnlockController;
+ @Mock
+ private UserTracker mUserTracker;
@Mock
private DozeMachine mDozeMachine;
@@ -89,12 +93,14 @@
when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
when(mBiometricUnlockController.hasPendingAuthentication()).thenReturn(false);
when(mDozeHost.isProvisioned()).thenReturn(true);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
mDozeSuppressor = new DozeSuppressor(
mDozeHost,
mConfig,
mDozeLog,
- mBiometricUnlockControllerLazy);
+ mBiometricUnlockControllerLazy,
+ mUserTracker);
mDozeSuppressor.setDozeMachine(mDozeMachine);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index b66a454..3552399 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -395,6 +395,14 @@
verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
}
+
+ @Test
+ public void udfpsLongPress_dozeState_notRegistered() {
+ // GIVEN device is DOZE_AOD_PAUSED
+ when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+ // beverlyt
+ }
+
private void waitForSensorManager() {
mExecutor.runAllReady();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 2799a25..6b095ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -36,11 +36,10 @@
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dreams.complication.ComplicationHostViewController;
+import com.android.systemui.dreams.touch.scrim.BouncerlessScrimController;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
import com.android.systemui.statusbar.BlurUtils;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import org.junit.Before;
import org.junit.Test;
@@ -81,12 +80,6 @@
BlurUtils mBlurUtils;
@Mock
- StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-
- @Mock
- KeyguardBouncer mBouncer;
-
- @Mock
ViewRootImpl mViewRoot;
@Mock
@@ -96,6 +89,9 @@
DreamOverlayAnimationsController mAnimationsController;
@Mock
+ BouncerlessScrimController mBouncerlessScrimController;
+
+ @Mock
DreamOverlayStateController mStateController;
DreamOverlayContainerViewController mController;
@@ -106,7 +102,6 @@
when(mDreamOverlayContainerView.getResources()).thenReturn(mResources);
when(mDreamOverlayContainerView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
- when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn(mBouncer);
when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
mController = new DreamOverlayContainerViewController(
@@ -114,7 +109,6 @@
mComplicationHostViewController,
mDreamOverlayContentView,
mDreamOverlayStatusBarViewController,
- mStatusBarKeyguardViewManager,
mBlurUtils,
mHandler,
mResources,
@@ -123,7 +117,8 @@
MILLIS_UNTIL_FULL_JITTER,
mPrimaryBouncerCallbackInteractor,
mAnimationsController,
- mStateController);
+ mStateController,
+ mBouncerlessScrimController);
}
@Test
@@ -170,7 +165,8 @@
final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
mController.onViewAttached();
- verify(mBouncer).addBouncerExpansionCallback(bouncerExpansionCaptor.capture());
+ verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
+ bouncerExpansionCaptor.capture());
bouncerExpansionCaptor.getValue().onExpansionChanged(0.5f);
verify(mBlurUtils, never()).applyBlur(eq(mViewRoot), anyInt(), eq(false));
@@ -181,7 +177,8 @@
final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
mController.onViewAttached();
- verify(mBouncer).addBouncerExpansionCallback(bouncerExpansionCaptor.capture());
+ verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
+ bouncerExpansionCaptor.capture());
final float blurRadius = 1337f;
when(mBlurUtils.blurRadiusOfRatio(anyFloat())).thenReturn(blurRadius);
@@ -217,6 +214,27 @@
}
@Test
+ public void testSkipEntryAnimationsWhenExitingLowLight() {
+ ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor =
+ ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+ when(mStateController.isLowLightActive()).thenReturn(false);
+
+ // Call onInit so that the callback is added.
+ mController.onInit();
+ verify(mStateController).addCallback(callbackCaptor.capture());
+
+ // Send the signal that low light is exiting
+ callbackCaptor.getValue().onExitLowLight();
+
+ // View is attached to trigger animations.
+ mController.onViewAttached();
+
+ // Entry animations should be started then immediately ended to skip to the end.
+ verify(mAnimationsController).startEntryAnimations();
+ verify(mAnimationsController).endAnimations();
+ }
+
+ @Test
public void testCancelDreamEntryAnimationsOnDetached() {
mController.onViewAttached();
mController.onViewDetached();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 4568d1e..dfb4d5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -31,6 +31,8 @@
import android.os.RemoteException;
import android.service.dreams.IDreamOverlay;
import android.service.dreams.IDreamOverlayCallback;
+import android.service.dreams.IDreamOverlayClient;
+import android.service.dreams.IDreamOverlayClientCallback;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.view.ViewGroup;
@@ -58,6 +60,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@SmallTest
@@ -148,13 +151,25 @@
mDreamOverlayCallbackController);
}
- @Test
- public void testOnStartMetricsLogged() throws Exception {
+ public IDreamOverlayClient getClient() throws RemoteException {
final IBinder proxy = mService.onBind(new Intent());
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClientCallback callback =
+ Mockito.mock(IDreamOverlayClientCallback.class);
+ overlay.getClient(callback);
+ final ArgumentCaptor<IDreamOverlayClient> clientCaptor =
+ ArgumentCaptor.forClass(IDreamOverlayClient.class);
+ verify(callback).onDreamOverlayClient(clientCaptor.capture());
+
+ return clientCaptor.getValue();
+ }
+
+ @Test
+ public void testOnStartMetricsLogged() throws Exception {
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -165,11 +180,10 @@
@Test
public void testOverlayContainerViewAddedToWindow() throws Exception {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -178,11 +192,10 @@
@Test
public void testDreamOverlayContainerViewControllerInitialized() throws Exception {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -196,11 +209,10 @@
.thenReturn(mDreamOverlayContainerViewParent)
.thenReturn(null);
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -209,11 +221,10 @@
@Test
public void testShouldShowComplicationsSetByStartDream() throws RemoteException {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
true /*shouldShowComplication*/);
assertThat(mService.shouldShowComplications()).isTrue();
@@ -221,11 +232,10 @@
@Test
public void testLowLightSetByStartDream() throws RemoteException {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback,
+ client.startDream(mWindowParams, mDreamOverlayCallback,
LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -234,12 +244,36 @@
}
@Test
- public void testDestroy() throws RemoteException {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ public void testOnEndDream() throws RemoteException {
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback,
+ client.startDream(mWindowParams, mDreamOverlayCallback,
+ LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
+ mMainExecutor.runAllReady();
+
+ // Verify view added.
+ verify(mWindowManager).addView(mViewCaptor.capture(), any());
+
+ // Service destroyed.
+ mService.onEndDream();
+ mMainExecutor.runAllReady();
+
+ // Verify view removed.
+ verify(mWindowManager).removeView(mViewCaptor.getValue());
+
+ // Verify state correctly set.
+ verify(mStateController).setOverlayActive(false);
+ verify(mStateController).setLowLightActive(false);
+ verify(mStateController).setEntryAnimationsFinished(false);
+ }
+
+ @Test
+ public void testDestroy() throws RemoteException {
+ final IDreamOverlayClient client = getClient();
+
+ // Inform the overlay service of dream starting.
+ client.startDream(mWindowParams, mDreamOverlayCallback,
LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -279,15 +313,14 @@
@Test
public void testDecorViewNotAddedToWindowAfterDestroy() throws Exception {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Destroy the service.
mService.onDestroy();
mMainExecutor.runAllReady();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -305,11 +338,10 @@
@Test
public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting. Do not show dream complications.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
false /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -326,7 +358,7 @@
// New dream starting with dream complications showing. Note that when a new dream is
// binding to the dream overlay service, it receives the same instance of IBinder as the
// first one.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
true /*shouldShowComplication*/);
mMainExecutor.runAllReady();
@@ -345,11 +377,10 @@
@Test
public void testWakeUp() throws RemoteException {
- final IBinder proxy = mService.onBind(new Intent());
- final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+ final IDreamOverlayClient client = getClient();
// Inform the overlay service of dream starting.
- overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+ client.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
true /*shouldShowComplication*/);
mMainExecutor.runAllReady();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index ee989d1..b7d0f29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -251,6 +251,30 @@
}
@Test
+ public void testNotifyLowLightExit() {
+ final DreamOverlayStateController stateController =
+ new DreamOverlayStateController(mExecutor, true);
+
+ stateController.addCallback(mCallback);
+ mExecutor.runAllReady();
+ assertThat(stateController.isLowLightActive()).isFalse();
+
+ // Turn low light on then off to trigger the exiting callback.
+ stateController.setLowLightActive(true);
+ stateController.setLowLightActive(false);
+
+ // Callback was only called once, when
+ mExecutor.runAllReady();
+ verify(mCallback, times(1)).onExitLowLight();
+ assertThat(stateController.isLowLightActive()).isFalse();
+
+ // Set with false again, which should not cause the callback to trigger again.
+ stateController.setLowLightActive(false);
+ mExecutor.runAllReady();
+ verify(mCallback, times(1)).onExitLowLight();
+ }
+
+ @Test
public void testNotifyEntryAnimationsFinishedChanged() {
final DreamOverlayStateController stateController =
new DreamOverlayStateController(mExecutor, true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 85c2819..596b903 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -31,6 +31,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.AlarmManager;
import android.content.Context;
import android.content.res.Resources;
@@ -47,6 +48,7 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.ZenModeController;
@@ -109,6 +111,8 @@
View mStatusBarItemView;
@Mock
DreamOverlayStateController mDreamOverlayStateController;
+ @Mock
+ UserTracker mUserTracker;
@Captor
private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
@@ -125,6 +129,7 @@
.thenReturn(NOTIFICATION_INDICATOR_FORMATTER_STRING);
doCallRealMethod().when(mView).setVisibility(anyInt());
doCallRealMethod().when(mView).getVisibility();
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
mController = new DreamOverlayStatusBarViewController(
mView,
@@ -140,7 +145,8 @@
mZenModeController,
mStatusBarWindowStateController,
mDreamOverlayStatusBarItemsProvider,
- mDreamOverlayStateController);
+ mDreamOverlayStateController,
+ mUserTracker);
}
@Test
@@ -282,7 +288,8 @@
mZenModeController,
mStatusBarWindowStateController,
mDreamOverlayStatusBarItemsProvider,
- mDreamOverlayStateController);
+ mDreamOverlayStateController,
+ mUserTracker);
controller.onViewAttached();
verify(mView, never()).showIcon(
eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index 89c7280..a4cf15c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -31,13 +31,13 @@
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.view.View;
-import android.widget.ImageView;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.common.ui.view.LaunchableImageView;
import com.android.systemui.controls.ControlsServiceInfo;
import com.android.systemui.controls.controller.ControlsController;
import com.android.systemui.controls.controller.StructureInfo;
@@ -90,7 +90,7 @@
private View mView;
@Mock
- private ImageView mHomeControlsView;
+ private LaunchableImageView mHomeControlsView;
@Mock
private ActivityStarter mActivityStarter;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
new file mode 100644
index 0000000..19347c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.dreams.conditions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.condition.Condition;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DreamConditionTest extends SysuiTestCase {
+ @Mock
+ Context mContext;
+
+ @Mock
+ Condition.Callback mCallback;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ /**
+ * Ensure a dreaming state immediately triggers the condition.
+ */
+ @Test
+ public void testInitialState() {
+ final Intent intent = new Intent(Intent.ACTION_DREAMING_STARTED);
+ when(mContext.registerReceiver(any(), any())).thenReturn(intent);
+ final DreamCondition condition = new DreamCondition(mContext);
+ condition.addCallback(mCallback);
+ condition.start();
+
+ verify(mCallback).onConditionChanged(eq(condition));
+ assertThat(condition.isConditionMet()).isTrue();
+ }
+
+ /**
+ * Ensure that changing dream state triggers condition.
+ */
+ @Test
+ public void testChange() {
+ final Intent intent = new Intent(Intent.ACTION_DREAMING_STARTED);
+ final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ when(mContext.registerReceiver(receiverCaptor.capture(), any())).thenReturn(intent);
+ final DreamCondition condition = new DreamCondition(mContext);
+ condition.addCallback(mCallback);
+ condition.start();
+ clearInvocations(mCallback);
+ receiverCaptor.getValue().onReceive(mContext, new Intent(Intent.ACTION_DREAMING_STOPPED));
+ verify(mCallback).onConditionChanged(eq(condition));
+ assertThat(condition.isConditionMet()).isFalse();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
index 4bd53c0..3a168d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java
@@ -41,12 +41,13 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.touch.scrim.ScrimController;
+import com.android.systemui.dreams.touch.scrim.ScrimManager;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.wm.shell.animation.FlingAnimationUtils;
import org.junit.Before;
@@ -63,10 +64,13 @@
@RunWith(AndroidTestingRunner.class)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@Mock
- StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ CentralSurfaces mCentralSurfaces;
@Mock
- CentralSurfaces mCentralSurfaces;
+ ScrimManager mScrimManager;
+
+ @Mock
+ ScrimController mScrimController;
@Mock
NotificationShadeWindowController mNotificationShadeWindowController;
@@ -111,7 +115,7 @@
MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler(
mDisplayMetrics,
- mStatusBarKeyguardViewManager,
+ mScrimManager,
Optional.of(mCentralSurfaces),
mNotificationShadeWindowController,
mValueAnimatorCreator,
@@ -121,6 +125,7 @@
TOUCH_REGION,
mUiEventLogger);
+ when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
when(mCentralSurfaces.getDisplayHeight()).thenReturn((float) SCREEN_HEIGHT_PX);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -193,7 +198,7 @@
assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
.isTrue();
- verify(mStatusBarKeyguardViewManager, never()).onPanelExpansionChanged(any());
+ verify(mScrimController, never()).expand(any());
}
/**
@@ -220,7 +225,7 @@
assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
.isTrue();
- verify(mStatusBarKeyguardViewManager, never()).onPanelExpansionChanged(any());
+ verify(mScrimController, never()).expand(any());
}
/**
@@ -274,12 +279,12 @@
final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
- reset(mStatusBarKeyguardViewManager);
+ reset(mScrimController);
assertThat(gestureListener.onScroll(event1, event2, 0, distanceY))
.isTrue();
// Ensure only called once
- verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(any());
+ verify(mScrimController).expand(any());
final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent;
final float dragDownAmount = event2.getY() - event1.getY();
@@ -288,7 +293,7 @@
ShadeExpansionChangeEvent event =
new ShadeExpansionChangeEvent(
expansion, /* expanded= */ false, /* tracking= */ true, dragDownAmount);
- verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(event);
+ verify(mScrimController).expand(event);
}
/**
@@ -302,12 +307,13 @@
final float velocityY = -1;
swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
- verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_HIDDEN));
+ verify(mValueAnimatorCreator).create(eq(expansion),
+ eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
verify(mValueAnimator, never()).addListener(any());
verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
eq(SCREEN_HEIGHT_PX * expansion),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_HIDDEN),
+ eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
verify(mValueAnimator).start();
verify(mUiEventLogger, never()).log(any());
@@ -324,7 +330,8 @@
final float velocityY = 1;
swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
- verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_VISIBLE));
+ verify(mValueAnimatorCreator).create(eq(expansion),
+ eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor =
ArgumentCaptor.forClass(AnimatorListenerAdapter.class);
@@ -332,7 +339,7 @@
AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue();
verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE),
+ eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
verify(mValueAnimator).start();
verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED);
@@ -355,12 +362,12 @@
swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
- eq(KeyguardBouncer.EXPANSION_VISIBLE));
+ eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
verify(mValueAnimator, never()).addListener(any());
verify(mFlingAnimationUtils).apply(eq(mValueAnimator),
eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE),
+ eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
verify(mValueAnimator).start();
verify(mUiEventLogger, never()).log(any());
@@ -381,12 +388,12 @@
swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
- eq(KeyguardBouncer.EXPANSION_HIDDEN));
+ eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
verify(mValueAnimator, never()).addListener(any());
verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_HIDDEN),
+ eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
verify(mValueAnimator).start();
verify(mUiEventLogger, never()).log(any());
@@ -405,7 +412,8 @@
final float velocityY = -1;
swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
- verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncer.EXPANSION_VISIBLE));
+ verify(mValueAnimatorCreator).create(eq(expansion),
+ eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
ArgumentCaptor<AnimatorListenerAdapter> endAnimationListenerCaptor =
ArgumentCaptor.forClass(AnimatorListenerAdapter.class);
@@ -413,7 +421,7 @@
AnimatorListenerAdapter endAnimationListener = endAnimationListenerCaptor.getValue();
verify(mFlingAnimationUtils).apply(eq(mValueAnimator), eq(SCREEN_HEIGHT_PX * expansion),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncer.EXPANSION_VISIBLE),
+ eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
verify(mValueAnimator).start();
verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_SWIPED);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
index a807407..178b9cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
@@ -424,6 +424,32 @@
verify(gestureListener2).onDown(eq(followupEvent));
}
+ @Test
+ public void testOnRemovedCallbackOnStopMonitoring() {
+ final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
+ final DreamTouchHandler.TouchSession.Callback callback =
+ Mockito.mock(DreamTouchHandler.TouchSession.Callback.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)));
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ final DreamTouchHandler.TouchSession session = captureSession(touchHandler);
+ session.registerCallback(callback);
+
+ environment.executeAll();
+
+ environment.updateLifecycle(observerOwnerPair -> {
+ observerOwnerPair.first.onPause(observerOwnerPair.second);
+ });
+
+ environment.executeAll();
+
+ verify(callback).onRemoved();
+ }
+
public GestureDetector.OnGestureListener registerGestureListener(DreamTouchHandler handler) {
final GestureDetector.OnGestureListener gestureListener = Mockito.mock(
GestureDetector.OnGestureListener.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
new file mode 100644
index 0000000..79c535a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.os.PowerManager;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class BouncerlessScrimControllerTest extends SysuiTestCase {
+ @Mock
+ BouncerlessScrimController.Callback mCallback;
+
+ @Mock
+ PowerManager mPowerManager;
+
+ private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testWakeupOnSwipeOpen() {
+ final BouncerlessScrimController scrimController =
+ new BouncerlessScrimController(mExecutor, mPowerManager);
+ scrimController.addCallback(mCallback);
+ scrimController.expand(new ShadeExpansionChangeEvent(.5f, true, false, 0.0f));
+ mExecutor.runAllReady();
+ verify(mPowerManager).wakeUp(anyLong(), eq(PowerManager.WAKE_REASON_GESTURE), any());
+ verify(mCallback).onWakeup();
+ }
+
+ @Test
+ public void testExpansionPropagation() {
+ final BouncerlessScrimController scrimController =
+ new BouncerlessScrimController(mExecutor, mPowerManager);
+ scrimController.addCallback(mCallback);
+ final ShadeExpansionChangeEvent expansionEvent =
+ new ShadeExpansionChangeEvent(0.5f, false, false, 0.0f);
+ scrimController.expand(expansionEvent);
+ mExecutor.runAllReady();
+ verify(mCallback).onExpansion(eq(expansionEvent));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
new file mode 100644
index 0000000..ac9822d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.dreams.touch.scrim;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ScrimManagerTest extends SysuiTestCase {
+ @Mock
+ ScrimController mBouncerlessScrimController;
+
+ @Mock
+ ScrimController mBouncerScrimController;
+
+ @Mock
+ KeyguardStateController mKeyguardStateController;
+
+ @Mock
+ ScrimManager.Callback mCallback;
+
+ private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testControllerSelection() {
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+ ArgumentCaptor<KeyguardStateController.Callback> callbackCaptor =
+ ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+ final ScrimManager manager = new ScrimManager(mExecutor, mBouncerScrimController,
+ mBouncerlessScrimController, mKeyguardStateController);
+ verify(mKeyguardStateController).addCallback(callbackCaptor.capture());
+
+ assertThat(manager.getCurrentController()).isEqualTo(mBouncerScrimController);
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+ callbackCaptor.getValue().onKeyguardShowingChanged();
+ mExecutor.runAllReady();
+ assertThat(manager.getCurrentController()).isEqualTo(mBouncerlessScrimController);
+ }
+
+ @Test
+ public void testCallback() {
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+ ArgumentCaptor<KeyguardStateController.Callback> callbackCaptor =
+ ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+ final ScrimManager manager = new ScrimManager(mExecutor, mBouncerScrimController,
+ mBouncerlessScrimController, mKeyguardStateController);
+ verify(mKeyguardStateController).addCallback(callbackCaptor.capture());
+
+ manager.addCallback(mCallback);
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+ callbackCaptor.getValue().onKeyguardShowingChanged();
+ mExecutor.runAllReady();
+ verify(mCallback).onScrimControllerChanged(eq(mBouncerlessScrimController));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
index 170a70f..35f0f6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
@@ -125,7 +125,7 @@
flags.set(unreleasedFlag, false)
flags.set(unreleasedFlag, false)
- listener.verifyInOrder(unreleasedFlag.id, unreleasedFlag.id)
+ listener.verifyInOrder(unreleasedFlag.name, unreleasedFlag.name)
}
@Test
@@ -137,7 +137,7 @@
flags.set(stringFlag, "Test")
flags.set(stringFlag, "Test")
- listener.verifyInOrder(stringFlag.id)
+ listener.verifyInOrder(stringFlag.name)
}
@Test
@@ -149,7 +149,7 @@
flags.removeListener(listener)
flags.set(unreleasedFlag, false)
- listener.verifyInOrder(unreleasedFlag.id)
+ listener.verifyInOrder(unreleasedFlag.name)
}
@Test
@@ -162,7 +162,7 @@
flags.removeListener(listener)
flags.set(stringFlag, "Other")
- listener.verifyInOrder(stringFlag.id)
+ listener.verifyInOrder(stringFlag.name)
}
@Test
@@ -175,7 +175,7 @@
flags.set(releasedFlag, true)
flags.set(unreleasedFlag, true)
- listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
+ listener.verifyInOrder(releasedFlag.name, unreleasedFlag.name)
}
@Test
@@ -191,7 +191,7 @@
flags.set(releasedFlag, false)
flags.set(unreleasedFlag, false)
- listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
+ listener.verifyInOrder(releasedFlag.name, unreleasedFlag.name)
}
@Test
@@ -204,8 +204,8 @@
flags.set(releasedFlag, true)
- listener1.verifyInOrder(releasedFlag.id)
- listener2.verifyInOrder(releasedFlag.id)
+ listener1.verifyInOrder(releasedFlag.name)
+ listener2.verifyInOrder(releasedFlag.name)
}
@Test
@@ -220,18 +220,18 @@
flags.removeListener(listener2)
flags.set(releasedFlag, false)
- listener1.verifyInOrder(releasedFlag.id, releasedFlag.id)
- listener2.verifyInOrder(releasedFlag.id)
+ listener1.verifyInOrder(releasedFlag.name, releasedFlag.name)
+ listener2.verifyInOrder(releasedFlag.name)
}
class VerifyingListener : FlagListenable.Listener {
- var flagEventIds = mutableListOf<Int>()
+ var flagEventNames = mutableListOf<String>()
override fun onFlagChanged(event: FlagListenable.FlagEvent) {
- flagEventIds.add(event.flagId)
+ flagEventNames.add(event.flagName)
}
- fun verifyInOrder(vararg eventIds: Int) {
- assertThat(flagEventIds).containsExactlyElementsIn(eventIds.asList())
+ fun verifyInOrder(vararg eventNames: String) {
+ assertThat(flagEventNames).containsExactlyElementsIn(eventNames.asList())
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
index 7592cc5..d8bbd04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
@@ -23,12 +23,11 @@
import android.content.res.Resources.NotFoundException
import android.test.suitebuilder.annotation.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.commandline.CommandRegistry
-import com.android.systemui.util.DeviceConfigProxyFake
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
@@ -62,21 +61,20 @@
@Mock
private lateinit var mockContext: Context
@Mock
+ private lateinit var globalSettings: GlobalSettings
+ @Mock
private lateinit var secureSettings: SecureSettings
@Mock
private lateinit var systemProperties: SystemPropertiesHelper
@Mock
private lateinit var resources: Resources
@Mock
- private lateinit var commandRegistry: CommandRegistry
- @Mock
private lateinit var restarter: Restarter
- private val flagMap = mutableMapOf<Int, Flag<*>>()
+ private val flagMap = mutableMapOf<String, Flag<*>>()
private lateinit var broadcastReceiver: BroadcastReceiver
- private lateinit var clearCacheAction: Consumer<Int>
+ private lateinit var clearCacheAction: Consumer<String>
private val serverFlagReader = ServerFlagReaderFake()
- private val deviceConfig = DeviceConfigProxyFake()
private val teamfoodableFlagA = UnreleasedFlag(
500, name = "a", namespace = "test", teamfood = true
)
@@ -87,11 +85,13 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- flagMap.put(teamfoodableFlagA.id, teamfoodableFlagA)
- flagMap.put(teamfoodableFlagB.id, teamfoodableFlagB)
+ flagMap.put(Flags.TEAMFOOD.name, Flags.TEAMFOOD)
+ flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA)
+ flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB)
mFeatureFlagsDebug = FeatureFlagsDebug(
flagManager,
mockContext,
+ globalSettings,
secureSettings,
systemProperties,
resources,
@@ -110,14 +110,14 @@
clearCacheAction = withArgCaptor {
verify(flagManager).clearCacheAction = capture()
}
- whenever(flagManager.idToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
+ whenever(flagManager.nameToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
}
@Test
fun readBooleanFlag() {
// Remember that the TEAMFOOD flag is id#1 and has special behavior.
- whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
- whenever(flagManager.readFlagValue<Boolean>(eq(4), any())).thenReturn(false)
+ whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true)
+ whenever(flagManager.readFlagValue<Boolean>(eq("4"), any())).thenReturn(false)
assertThat(
mFeatureFlagsDebug.isEnabled(
@@ -141,7 +141,7 @@
mFeatureFlagsDebug.isEnabled(
ReleasedFlag(
4,
- name = "3",
+ name = "4",
namespace = "test"
)
)
@@ -150,7 +150,7 @@
mFeatureFlagsDebug.isEnabled(
UnreleasedFlag(
5,
- name = "4",
+ name = "5",
namespace = "test"
)
)
@@ -159,7 +159,8 @@
@Test
fun teamFoodFlag_False() {
- whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(false)
+ whenever(flagManager.readFlagValue<Boolean>(
+ eq(Flags.TEAMFOOD.name), any())).thenReturn(false)
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
@@ -170,7 +171,8 @@
@Test
fun teamFoodFlag_True() {
- whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
+ whenever(flagManager.readFlagValue<Boolean>(
+ eq(Flags.TEAMFOOD.name), any())).thenReturn(true)
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
@@ -181,11 +183,12 @@
@Test
fun teamFoodFlag_Overridden() {
- whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.id), any()))
+ whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.name), any()))
.thenReturn(true)
- whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.id), any()))
+ whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any()))
.thenReturn(false)
- whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
+ whenever(flagManager.readFlagValue<Boolean>(
+ eq(Flags.TEAMFOOD.name), any())).thenReturn(true)
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse()
@@ -202,8 +205,8 @@
whenever(resources.getBoolean(1004)).thenAnswer { throw NameNotFoundException() }
whenever(resources.getBoolean(1005)).thenAnswer { throw NameNotFoundException() }
- whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
- whenever(flagManager.readFlagValue<Boolean>(eq(5), any())).thenReturn(false)
+ whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true)
+ whenever(flagManager.readFlagValue<Boolean>(eq("5"), any())).thenReturn(false)
assertThat(
mFeatureFlagsDebug.isEnabled(
@@ -255,8 +258,8 @@
@Test
fun readStringFlag() {
- whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("foo")
- whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("bar")
+ whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("foo")
+ whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("bar")
assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "1", "test", "biz"))).isEqualTo("biz")
assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "2", "test", "baz"))).isEqualTo("baz")
assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "3", "test", "buz"))).isEqualTo("foo")
@@ -272,9 +275,9 @@
whenever(resources.getString(1005)).thenAnswer { throw NameNotFoundException() }
whenever(resources.getString(1006)).thenAnswer { throw NameNotFoundException() }
- whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("override3")
- whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("override4")
- whenever(flagManager.readFlagValue<String>(eq(6), any())).thenReturn("override6")
+ whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("override3")
+ whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("override4")
+ whenever(flagManager.readFlagValue<String>(eq("6"), any())).thenReturn("override6")
assertThat(
mFeatureFlagsDebug.getString(
@@ -322,8 +325,8 @@
@Test
fun readIntFlag() {
- whenever(flagManager.readFlagValue<Int>(eq(3), any())).thenReturn(22)
- whenever(flagManager.readFlagValue<Int>(eq(4), any())).thenReturn(48)
+ whenever(flagManager.readFlagValue<Int>(eq("3"), any())).thenReturn(22)
+ whenever(flagManager.readFlagValue<Int>(eq("4"), any())).thenReturn(48)
assertThat(mFeatureFlagsDebug.getInt(IntFlag(1, "1", "test", 12))).isEqualTo(12)
assertThat(mFeatureFlagsDebug.getInt(IntFlag(2, "2", "test", 93))).isEqualTo(93)
assertThat(mFeatureFlagsDebug.getInt(IntFlag(3, "3", "test", 8))).isEqualTo(22)
@@ -368,12 +371,12 @@
broadcastReceiver.onReceive(mockContext, Intent())
broadcastReceiver.onReceive(mockContext, Intent("invalid action"))
broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG))
- setByBroadcast(0, false) // unknown id does nothing
- setByBroadcast(1, "string") // wrong type does nothing
- setByBroadcast(2, 123) // wrong type does nothing
- setByBroadcast(3, false) // wrong type does nothing
- setByBroadcast(4, 123) // wrong type does nothing
- verifyNoMoreInteractions(flagManager, secureSettings)
+ setByBroadcast("0", false) // unknown id does nothing
+ setByBroadcast("1", "string") // wrong type does nothing
+ setByBroadcast("2", 123) // wrong type does nothing
+ setByBroadcast("3", false) // wrong type does nothing
+ setByBroadcast("4", 123) // wrong type does nothing
+ verifyNoMoreInteractions(flagManager, globalSettings)
}
@Test
@@ -383,16 +386,16 @@
// trying to erase an id not in the map does nothing
broadcastReceiver.onReceive(
mockContext,
- Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 0)
+ Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "")
)
- verifyNoMoreInteractions(flagManager, secureSettings)
+ verifyNoMoreInteractions(flagManager, globalSettings)
// valid id with no value puts empty string in the setting
broadcastReceiver.onReceive(
mockContext,
- Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 1)
+ Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "1")
)
- verifyPutData(1, "", numReads = 0)
+ verifyPutData("1", "", numReads = 0)
}
@Test
@@ -402,51 +405,51 @@
addFlag(ResourceBooleanFlag(3, "3", "test", 1003))
addFlag(ResourceBooleanFlag(4, "4", "test", 1004))
- setByBroadcast(1, false)
- verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}")
+ setByBroadcast("1", false)
+ verifyPutData("1", "{\"type\":\"boolean\",\"value\":false}")
- setByBroadcast(2, true)
- verifyPutData(2, "{\"type\":\"boolean\",\"value\":true}")
+ setByBroadcast("2", true)
+ verifyPutData("2", "{\"type\":\"boolean\",\"value\":true}")
- setByBroadcast(3, false)
- verifyPutData(3, "{\"type\":\"boolean\",\"value\":false}")
+ setByBroadcast("3", false)
+ verifyPutData("3", "{\"type\":\"boolean\",\"value\":false}")
- setByBroadcast(4, true)
- verifyPutData(4, "{\"type\":\"boolean\",\"value\":true}")
+ setByBroadcast("4", true)
+ verifyPutData("4", "{\"type\":\"boolean\",\"value\":true}")
}
@Test
fun setStringFlag() {
- addFlag(StringFlag(1, "flag1", "1", "test"))
+ addFlag(StringFlag(1, "1", "1", "test"))
addFlag(ResourceStringFlag(2, "2", "test", 1002))
- setByBroadcast(1, "override1")
- verifyPutData(1, "{\"type\":\"string\",\"value\":\"override1\"}")
+ setByBroadcast("1", "override1")
+ verifyPutData("1", "{\"type\":\"string\",\"value\":\"override1\"}")
- setByBroadcast(2, "override2")
- verifyPutData(2, "{\"type\":\"string\",\"value\":\"override2\"}")
+ setByBroadcast("2", "override2")
+ verifyPutData("2", "{\"type\":\"string\",\"value\":\"override2\"}")
}
@Test
fun setFlag_ClearsCache() {
val flag1 = addFlag(StringFlag(1, "1", "test", "flag1"))
- whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("original")
+ whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("original")
// gets the flag & cache it
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
- verify(flagManager).readFlagValue(eq(1), eq(StringFlagSerializer))
+ verify(flagManager, times(1)).readFlagValue(eq("1"), eq(StringFlagSerializer))
// hit the cache
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
verifyNoMoreInteractions(flagManager)
// set the flag
- setByBroadcast(1, "new")
- verifyPutData(1, "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
- whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("new")
+ setByBroadcast("1", "new")
+ verifyPutData("1", "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
+ whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("new")
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("new")
- verify(flagManager, times(3)).readFlagValue(eq(1), eq(StringFlagSerializer))
+ verify(flagManager, times(3)).readFlagValue(eq("1"), eq(StringFlagSerializer))
}
@Test
@@ -463,7 +466,6 @@
val flag = UnreleasedFlag(100, name = "100", namespace = "test")
serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
-
assertThat(mFeatureFlagsDebug.isEnabled(flag)).isTrue()
}
@@ -503,26 +505,26 @@
assertThat(dump).contains(" sysui_flag_7: [length=9] \"override7\"\n")
}
- private fun verifyPutData(id: Int, data: String, numReads: Int = 1) {
- inOrder(flagManager, secureSettings).apply {
- verify(flagManager, times(numReads)).readFlagValue(eq(id), any<FlagSerializer<*>>())
- verify(flagManager).idToSettingsKey(eq(id))
- verify(secureSettings).putStringForUser(eq("key-$id"), eq(data), anyInt())
- verify(flagManager).dispatchListenersAndMaybeRestart(eq(id), any())
+ private fun verifyPutData(name: String, data: String, numReads: Int = 1) {
+ inOrder(flagManager, globalSettings).apply {
+ verify(flagManager, times(numReads)).readFlagValue(eq(name), any<FlagSerializer<*>>())
+ verify(flagManager).nameToSettingsKey(eq(name))
+ verify(globalSettings).putStringForUser(eq("key-$name"), eq(data), anyInt())
+ verify(flagManager).dispatchListenersAndMaybeRestart(eq(name), any())
}.verifyNoMoreInteractions()
- verifyNoMoreInteractions(flagManager, secureSettings)
+ verifyNoMoreInteractions(flagManager, globalSettings)
}
- private fun setByBroadcast(id: Int, value: Serializable?) {
+ private fun setByBroadcast(name: String, value: Serializable?) {
val intent = Intent(FlagManager.ACTION_SET_FLAG)
- intent.putExtra(FlagManager.EXTRA_ID, id)
+ intent.putExtra(FlagManager.EXTRA_NAME, name)
intent.putExtra(FlagManager.EXTRA_VALUE, value)
broadcastReceiver.onReceive(mockContext, intent)
}
private fun <F : Flag<*>> addFlag(flag: F): F {
- val old = flagMap.put(flag.id, flag)
- check(old == null) { "Flag ${flag.id} already registered" }
+ val old = flagMap.put(flag.name, flag)
+ check(old == null) { "Flag ${flag.name} already registered" }
return flag
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
index d5b5a4a..4c6028c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
@@ -19,7 +19,6 @@
import android.content.res.Resources
import android.test.suitebuilder.annotation.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.DeviceConfigProxyFake
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Before
@@ -39,9 +38,8 @@
@Mock private lateinit var mResources: Resources
@Mock private lateinit var mSystemProperties: SystemPropertiesHelper
@Mock private lateinit var restarter: Restarter
- private val flagMap = mutableMapOf<Int, Flag<*>>()
+ private val flagMap = mutableMapOf<String, Flag<*>>()
private val serverFlagReader = ServerFlagReaderFake()
- private val deviceConfig = DeviceConfigProxyFake()
@Before
fun setup() {
@@ -49,7 +47,6 @@
mFeatureFlagsRelease = FeatureFlagsRelease(
mResources,
mSystemProperties,
- deviceConfig,
serverFlagReader,
flagMap,
restarter)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index fea91c5..28131b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
@@ -32,7 +32,7 @@
@Mock private lateinit var featureFlags: FeatureFlagsDebug
@Mock private lateinit var pw: PrintWriter
- private val flagMap = mutableMapOf<Int, Flag<*>>()
+ private val flagMap = mutableMapOf<String, Flag<*>>()
private val flagA = UnreleasedFlag(500, "500", "test")
private val flagB = ReleasedFlag(501, "501", "test")
private val stringFlag = StringFlag(502, "502", "test", "abracadabra")
@@ -53,59 +53,59 @@
(invocation.getArgument(0) as IntFlag).default
}
- flagMap.put(flagA.id, flagA)
- flagMap.put(flagB.id, flagB)
- flagMap.put(stringFlag.id, stringFlag)
- flagMap.put(intFlag.id, intFlag)
+ flagMap.put(flagA.name, flagA)
+ flagMap.put(flagB.name, flagB)
+ flagMap.put(stringFlag.name, stringFlag)
+ flagMap.put(intFlag.name, intFlag)
cmd = FlagCommand(featureFlags, flagMap)
}
@Test
fun readBooleanFlagCommand() {
- cmd.execute(pw, listOf(flagA.id.toString()))
+ cmd.execute(pw, listOf(flagA.name))
Mockito.verify(featureFlags).isEnabled(flagA)
}
@Test
fun readStringFlagCommand() {
- cmd.execute(pw, listOf(stringFlag.id.toString()))
+ cmd.execute(pw, listOf(stringFlag.name))
Mockito.verify(featureFlags).getString(stringFlag)
}
@Test
fun readIntFlag() {
- cmd.execute(pw, listOf(intFlag.id.toString()))
+ cmd.execute(pw, listOf(intFlag.name))
Mockito.verify(featureFlags).getInt(intFlag)
}
@Test
fun setBooleanFlagCommand() {
- cmd.execute(pw, listOf(flagB.id.toString(), "on"))
+ cmd.execute(pw, listOf(flagB.name, "on"))
Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, true)
}
@Test
fun setStringFlagCommand() {
- cmd.execute(pw, listOf(stringFlag.id.toString(), "set", "foobar"))
+ cmd.execute(pw, listOf(stringFlag.name, "set", "foobar"))
Mockito.verify(featureFlags).setStringFlagInternal(stringFlag, "foobar")
}
@Test
fun setIntFlag() {
- cmd.execute(pw, listOf(intFlag.id.toString(), "put", "123"))
+ cmd.execute(pw, listOf(intFlag.name, "put", "123"))
Mockito.verify(featureFlags).setIntFlagInternal(intFlag, 123)
}
@Test
fun toggleBooleanFlagCommand() {
- cmd.execute(pw, listOf(flagB.id.toString(), "toggle"))
+ cmd.execute(pw, listOf(flagB.name, "toggle"))
Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, false)
}
@Test
fun eraseFlagCommand() {
- cmd.execute(pw, listOf(flagA.id.toString(), "erase"))
+ cmd.execute(pw, listOf(flagA.name, "erase"))
Mockito.verify(featureFlags).eraseFlag(flagA)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
index fca7e96..e679d47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
@@ -87,14 +87,14 @@
@Test
fun testObserverClearsCache() {
val listener = mock<FlagListenable.Listener>()
- val clearCacheAction = mock<Consumer<Int>>()
+ val clearCacheAction = mock<Consumer<String>>()
mFlagManager.clearCacheAction = clearCacheAction
mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
val observer = withArgCaptor<ContentObserver> {
verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
}
observer.onChange(false, flagUri(1))
- verify(clearCacheAction).accept(eq(1))
+ verify(clearCacheAction).accept(eq("1"))
}
@Test
@@ -110,14 +110,14 @@
val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
verify(listener1).onFlagChanged(capture())
}
- assertThat(flagEvent1.flagId).isEqualTo(1)
+ assertThat(flagEvent1.flagName).isEqualTo("1")
verifyNoMoreInteractions(listener1, listener10)
observer.onChange(false, flagUri(10))
val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
verify(listener10).onFlagChanged(capture())
}
- assertThat(flagEvent10.flagId).isEqualTo(10)
+ assertThat(flagEvent10.flagName).isEqualTo("10")
verifyNoMoreInteractions(listener1, listener10)
}
@@ -130,18 +130,18 @@
mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener10)
- mFlagManager.dispatchListenersAndMaybeRestart(1, null)
+ mFlagManager.dispatchListenersAndMaybeRestart("1", null)
val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
verify(listener1).onFlagChanged(capture())
}
- assertThat(flagEvent1.flagId).isEqualTo(1)
+ assertThat(flagEvent1.flagName).isEqualTo("1")
verifyNoMoreInteractions(listener1, listener10)
- mFlagManager.dispatchListenersAndMaybeRestart(10, null)
+ mFlagManager.dispatchListenersAndMaybeRestart("10", null)
val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
verify(listener10).onFlagChanged(capture())
}
- assertThat(flagEvent10.flagId).isEqualTo(10)
+ assertThat(flagEvent10.flagName).isEqualTo("10")
verifyNoMoreInteractions(listener1, listener10)
}
@@ -151,25 +151,25 @@
mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener)
- mFlagManager.dispatchListenersAndMaybeRestart(1, null)
+ mFlagManager.dispatchListenersAndMaybeRestart("1", null)
val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
verify(listener).onFlagChanged(capture())
}
- assertThat(flagEvent1.flagId).isEqualTo(1)
+ assertThat(flagEvent1.flagName).isEqualTo("1")
verifyNoMoreInteractions(listener)
- mFlagManager.dispatchListenersAndMaybeRestart(10, null)
+ mFlagManager.dispatchListenersAndMaybeRestart("10", null)
val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
verify(listener, times(2)).onFlagChanged(capture())
}
- assertThat(flagEvent10.flagId).isEqualTo(10)
+ assertThat(flagEvent10.flagName).isEqualTo("10")
verifyNoMoreInteractions(listener)
}
@Test
fun testRestartWithNoListeners() {
val restartAction = mock<Consumer<Boolean>>()
- mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+ mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
verify(restartAction).accept(eq(false))
verifyNoMoreInteractions(restartAction)
}
@@ -180,7 +180,7 @@
mFlagManager.addListener(ReleasedFlag(1, "1", "test")) { event ->
event.requestNoRestart()
}
- mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+ mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
verify(restartAction).accept(eq(true))
verifyNoMoreInteractions(restartAction)
}
@@ -191,7 +191,7 @@
mFlagManager.addListener(ReleasedFlag(10, "10", "test")) { event ->
event.requestNoRestart()
}
- mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+ mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
verify(restartAction).accept(eq(false))
verifyNoMoreInteractions(restartAction)
}
@@ -205,7 +205,7 @@
mFlagManager.addListener(ReleasedFlag(10, "10", "test")) {
// do not request
}
- mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
+ mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction)
verify(restartAction).accept(eq(false))
verifyNoMoreInteractions(restartAction)
}
@@ -214,31 +214,31 @@
fun testReadBooleanFlag() {
// test that null string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn(null)
- assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
+ assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull()
// test that empty string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn("")
- assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
+ assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull()
// test false
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"boolean\",\"value\":false}")
- assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isFalse()
+ assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isFalse()
// test true
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"boolean\",\"value\":true}")
- assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isTrue()
+ assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isTrue()
// Reading a value of a different type should just return null
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"string\",\"value\":\"foo\"}")
- assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
+ assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull()
// Reading a value that isn't json should throw an exception
assertThrows(InvalidFlagStorageException::class.java) {
whenever(mFlagSettingsHelper.getString(any())).thenReturn("1")
- mFlagManager.readFlagValue(1, BooleanFlagSerializer)
+ mFlagManager.readFlagValue("1", BooleanFlagSerializer)
}
}
@@ -257,31 +257,31 @@
fun testReadStringFlag() {
// test that null string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn(null)
- assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
+ assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull()
// test that empty string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn("")
- assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
+ assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull()
// test json with the empty string value returns empty string
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"string\",\"value\":\"\"}")
- assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isEqualTo("")
+ assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isEqualTo("")
// test string with value is returned
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"string\",\"value\":\"foo\"}")
- assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isEqualTo("foo")
+ assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isEqualTo("foo")
// Reading a value of a different type should just return null
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"boolean\",\"value\":false}")
- assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
+ assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull()
// Reading a value that isn't json should throw an exception
assertThrows(InvalidFlagStorageException::class.java) {
whenever(mFlagSettingsHelper.getString(any())).thenReturn("1")
- mFlagManager.readFlagValue(1, StringFlagSerializer)
+ mFlagManager.readFlagValue("1", StringFlagSerializer)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt
index 77c837b..a2dc1eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/fragments/FragmentServiceTest.kt
@@ -14,6 +14,7 @@
@SmallTest
class FragmentServiceTest : SysuiTestCase() {
private val fragmentCreator = TestFragmentCreator()
+ private val fragmenetHostManagerFactory: FragmentHostManager.Factory = mock()
private val fragmentCreatorFactory = FragmentService.FragmentCreator.Factory { fragmentCreator }
private lateinit var fragmentService: FragmentService
@@ -24,7 +25,13 @@
Looper.prepare()
}
- fragmentService = FragmentService(fragmentCreatorFactory, mock(), DumpManager())
+ fragmentService =
+ FragmentService(
+ fragmentCreatorFactory,
+ fragmenetHostManagerFactory,
+ mock(),
+ DumpManager()
+ )
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index 4659766..fb54d6d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -51,6 +51,7 @@
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
@@ -61,6 +62,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -86,9 +88,9 @@
@Mock private lateinit var backgroundHandler: Handler
@Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: CustomizationProvider
-
private lateinit var testScope: TestScope
@Before
@@ -160,6 +162,7 @@
keyguardInteractor =
KeyguardInteractor(
repository = FakeKeyguardRepository(),
+ commandQueue = commandQueue,
),
registry = mock(),
lockPatternUtils = lockPatternUtils,
@@ -171,6 +174,7 @@
set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
set(Flags.LOCKSCREEN_CUSTOM_CLOCKS, true)
set(Flags.REVAMPED_WALLPAPER_UI, true)
+ set(Flags.WALLPAPER_FULLSCREEN_PREVIEW, true)
},
repository = { quickAffordanceRepository },
launchAnimator = launchAnimator,
@@ -181,6 +185,7 @@
mainDispatcher = testDispatcher,
backgroundHandler = backgroundHandler,
)
+ underTest.mainDispatcher = UnconfinedTestDispatcher()
underTest.attachInfoForTesting(
context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 9b0d8db..f55b866 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -29,6 +29,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -66,6 +67,7 @@
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
@@ -135,6 +137,7 @@
private @Mock AuthController mAuthController;
private @Mock ShadeExpansionStateManager mShadeExpansionStateManager;
private @Mock ShadeWindowLogger mShadeWindowLogger;
+ private @Mock FeatureFlags mFeatureFlags;
private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
@@ -483,6 +486,38 @@
assertTrue(mViewMediator.isShowingAndNotOccluded());
}
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ public void testDoKeyguardWhileInteractive_resets() {
+ mViewMediator.setShowingLocked(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ TestableLooper.get(this).processAllMessages();
+
+ when(mPowerManager.isInteractive()).thenReturn(true);
+
+ mViewMediator.onSystemReady();
+ TestableLooper.get(this).processAllMessages();
+
+ assertTrue(mViewMediator.isShowingAndNotOccluded());
+ verify(mStatusBarKeyguardViewManager).reset(anyBoolean());
+ }
+
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() {
+ mViewMediator.setShowingLocked(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ TestableLooper.get(this).processAllMessages();
+
+ when(mPowerManager.isInteractive()).thenReturn(false);
+
+ mViewMediator.onSystemReady();
+ TestableLooper.get(this).processAllMessages();
+
+ assertTrue(mViewMediator.isShowingAndNotOccluded());
+ verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean());
+ }
+
private void createAndStartViewMediator() {
mViewMediator = new KeyguardViewMediator(
mContext,
@@ -510,6 +545,7 @@
mScreenOnCoordinator,
mInteractionJankMonitor,
mDreamOverlayStateController,
+ mFeatureFlags,
() -> mShadeController,
() -> mNotificationShadeWindowController,
() -> mActivityLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
index f32d76b..39a453d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
@@ -30,6 +30,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
@@ -51,7 +52,12 @@
public void setUp() throws Exception {
mWallpaperManager = mock(IWallpaperManager.class);
mWakefulness =
- new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class));
+ new WakefulnessLifecycle(
+ mContext,
+ mWallpaperManager,
+ new FakeSystemClock(),
+ mock(DumpManager.class)
+ );
mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class);
mWakefulness.addObserver(mWakefulnessObserver);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
index e9db8cc..b9cfc65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
@@ -23,6 +23,7 @@
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.Activity;
import android.app.ActivityManager;
@@ -35,12 +36,12 @@
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
-import android.os.UserHandle;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -71,6 +72,7 @@
private @Mock Context mContext;
private @Mock TaskStackChangeListeners mTaskStackChangeListeners;
private @Mock IActivityTaskManager mIActivityTaskManager;
+ private @Mock UserTracker mUserTracker;
private WorkLockActivityController mController;
private TaskStackChangeListener mTaskStackListener;
@@ -81,12 +83,13 @@
// Set a package name to use for checking ComponentName well-formedness in tests.
doReturn("com.example.test").when(mContext).getPackageName();
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
// Construct controller. Save the TaskStackListener for injecting events.
final ArgumentCaptor<TaskStackChangeListener> listenerCaptor =
ArgumentCaptor.forClass(TaskStackChangeListener.class);
- mController = new WorkLockActivityController(mContext, mTaskStackChangeListeners,
- mIActivityTaskManager);
+ mController = new WorkLockActivityController(mContext, mUserTracker,
+ mTaskStackChangeListeners, mIActivityTaskManager);
verify(mTaskStackChangeListeners).registerTaskStackListener(listenerCaptor.capture());
mTaskStackListener = listenerCaptor.getValue();
@@ -135,7 +138,7 @@
anyInt(),
eq((ProfilerInfo) null),
argThat(hasOptions(taskId, taskOverlay)),
- eq(UserHandle.USER_CURRENT));
+ eq(ActivityManager.getCurrentUser()));
}
private void verifyStartActivity(int taskId, boolean taskOverlay) throws Exception {
@@ -151,7 +154,7 @@
anyInt(),
eq((ProfilerInfo) null),
argThat(hasOptions(taskId, taskOverlay)),
- eq(UserHandle.USER_CURRENT));
+ eq(ActivityManager.getCurrentUser()));
}
private static ArgumentMatcher<Intent> hasComponent(final Context context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
index 7205f30..58cdec4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
@@ -19,9 +19,14 @@
import android.app.StatusBarManager
import android.content.Context
+import android.content.pm.PackageManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.camera.CameraGestureHelper
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -31,22 +36,26 @@
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class CameraQuickAffordanceConfigTest : SysuiTestCase() {
@Mock private lateinit var cameraGestureHelper: CameraGestureHelper
@Mock private lateinit var context: Context
+ @Mock private lateinit var packageManager: PackageManager
private lateinit var underTest: CameraQuickAffordanceConfig
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ setLaunchable(true)
underTest =
CameraQuickAffordanceConfig(
context,
+ packageManager,
) {
cameraGestureHelper
}
@@ -62,4 +71,25 @@
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
}
+
+ @Test
+ fun `getPickerScreenState - default when launchable`() = runTest {
+ setLaunchable(true)
+
+ Truth.assertThat(underTest.getPickerScreenState())
+ .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+ }
+
+ @Test
+ fun `getPickerScreenState - unavailable when not launchable`() = runTest {
+ setLaunchable(false)
+
+ Truth.assertThat(underTest.getPickerScreenState())
+ .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+ }
+
+ private fun setLaunchable(isLaunchable: Boolean) {
+ whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
+ .thenReturn(isLaunchable)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index 7c10108..15b85de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -38,6 +38,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
@@ -83,169 +84,205 @@
settings = FakeSettings()
- underTest = DoNotDisturbQuickAffordanceConfig(
- context,
- zenModeController,
- settings,
- userTracker,
- testDispatcher,
- conditionUri,
- enableZenModeDialog,
- )
+ underTest =
+ DoNotDisturbQuickAffordanceConfig(
+ context,
+ zenModeController,
+ settings,
+ userTracker,
+ testDispatcher,
+ conditionUri,
+ enableZenModeDialog,
+ )
}
@Test
- fun `dnd not available - picker state hidden`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(false)
+ fun `dnd not available - picker state hidden`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(false)
- //when
- val result = underTest.getPickerScreenState()
+ // when
+ val result = underTest.getPickerScreenState()
- //then
- assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result)
- }
+ // then
+ assertEquals(
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice,
+ result
+ )
+ }
@Test
- fun `dnd available - picker state visible`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(true)
+ fun `dnd available - picker state visible`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
- //when
- val result = underTest.getPickerScreenState()
+ // when
+ val result = underTest.getPickerScreenState()
- //then
- assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default, result)
- }
+ // then
+ assertThat(result)
+ .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+ val defaultPickerState =
+ result as KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ assertThat(defaultPickerState.configureIntent).isNotNull()
+ assertThat(defaultPickerState.configureIntent?.action)
+ .isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+ }
@Test
- fun `onTriggered - dnd mode is not ZEN_MODE_OFF - set to ZEN_MODE_OFF`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(true)
- whenever(zenModeController.zen).thenReturn(-1)
- settings.putInt(Settings.Secure.ZEN_DURATION, -2)
- collectLastValue(underTest.lockScreenState)
- runCurrent()
+ fun `onTriggered - dnd mode is not ZEN_MODE_OFF - set to ZEN_MODE_OFF`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
+ whenever(zenModeController.zen).thenReturn(-1)
+ settings.putInt(Settings.Secure.ZEN_DURATION, -2)
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
- //when
- val result = underTest.onTriggered(null)
- verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG))
+ // when
+ val result = underTest.onTriggered(null)
+ verify(zenModeController)
+ .setZen(
+ spyZenMode.capture(),
+ spyConditionId.capture(),
+ eq(DoNotDisturbQuickAffordanceConfig.TAG)
+ )
- //then
- assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
- assertEquals(ZEN_MODE_OFF, spyZenMode.value)
- assertNull(spyConditionId.value)
- }
+ // then
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ assertEquals(ZEN_MODE_OFF, spyZenMode.value)
+ assertNull(spyConditionId.value)
+ }
@Test
- fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is FOREVER - set zen with no condition`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(true)
- whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
- settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
- collectLastValue(underTest.lockScreenState)
- runCurrent()
+ fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting FOREVER - set zen without condition`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
+ whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+ settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
- //when
- val result = underTest.onTriggered(null)
- verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG))
+ // when
+ val result = underTest.onTriggered(null)
+ verify(zenModeController)
+ .setZen(
+ spyZenMode.capture(),
+ spyConditionId.capture(),
+ eq(DoNotDisturbQuickAffordanceConfig.TAG)
+ )
- //then
- assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
- assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
- assertNull(spyConditionId.value)
- }
+ // then
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
+ assertNull(spyConditionId.value)
+ }
@Test
- fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is not FOREVER or PROMPT - set zen with condition`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(true)
- whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
- settings.putInt(Settings.Secure.ZEN_DURATION, -900)
- collectLastValue(underTest.lockScreenState)
- runCurrent()
+ fun `onTriggered - dnd ZEN_MODE_OFF - setting not FOREVER or PROMPT - zen with condition`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
+ whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+ settings.putInt(Settings.Secure.ZEN_DURATION, -900)
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
- //when
- val result = underTest.onTriggered(null)
- verify(zenModeController).setZen(spyZenMode.capture(), spyConditionId.capture(), eq(DoNotDisturbQuickAffordanceConfig.TAG))
+ // when
+ val result = underTest.onTriggered(null)
+ verify(zenModeController)
+ .setZen(
+ spyZenMode.capture(),
+ spyConditionId.capture(),
+ eq(DoNotDisturbQuickAffordanceConfig.TAG)
+ )
- //then
- assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
- assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
- assertEquals(conditionUri, spyConditionId.value)
- }
+ // then
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value)
+ assertEquals(conditionUri, spyConditionId.value)
+ }
@Test
- fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is PROMPT - show dialog`() = testScope.runTest {
- //given
- val expandable: Expandable = mock()
- whenever(zenModeController.isZenAvailable).thenReturn(true)
- whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
- settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
- whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
- collectLastValue(underTest.lockScreenState)
- runCurrent()
+ fun `onTriggered - dnd mode is ZEN_MODE_OFF - setting is PROMPT - show dialog`() =
+ testScope.runTest {
+ // given
+ val expandable: Expandable = mock()
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
+ whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+ settings.putInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
+ whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
+ collectLastValue(underTest.lockScreenState)
+ runCurrent()
- //when
- val result = underTest.onTriggered(expandable)
+ // when
+ val result = underTest.onTriggered(expandable)
- //then
- assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
- assertEquals(expandable, (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable)
- }
+ // then
+ assertTrue(result is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog)
+ assertEquals(
+ expandable,
+ (result as KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog).expandable
+ )
+ }
@Test
- fun `lockScreenState - dndAvailable starts as true - changes to false - State moves to Hidden`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(true)
- val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
- val valueSnapshot = collectLastValue(underTest.lockScreenState)
- val secondLastValue = valueSnapshot()
- verify(zenModeController).addCallback(callbackCaptor.capture())
+ fun `lockScreenState - dndAvailable starts as true - change to false - State is Hidden`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
+ val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
+ val valueSnapshot = collectLastValue(underTest.lockScreenState)
+ val secondLastValue = valueSnapshot()
+ verify(zenModeController).addCallback(callbackCaptor.capture())
- //when
- callbackCaptor.value.onZenAvailableChanged(false)
- val lastValue = valueSnapshot()
+ // when
+ callbackCaptor.value.onZenAvailableChanged(false)
+ val lastValue = valueSnapshot()
- //then
- assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
- assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
- }
+ // then
+ assertTrue(secondLastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ }
@Test
- fun `lockScreenState - dndMode starts as ZEN_MODE_OFF - changes to not OFF - State moves to Visible`() = testScope.runTest {
- //given
- whenever(zenModeController.isZenAvailable).thenReturn(true)
- whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
- val valueSnapshot = collectLastValue(underTest.lockScreenState)
- val secondLastValue = valueSnapshot()
- val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
- verify(zenModeController).addCallback(callbackCaptor.capture())
+ fun `lockScreenState - dndMode starts as ZEN_MODE_OFF - change to not OFF - State Visible`() =
+ testScope.runTest {
+ // given
+ whenever(zenModeController.isZenAvailable).thenReturn(true)
+ whenever(zenModeController.zen).thenReturn(ZEN_MODE_OFF)
+ val valueSnapshot = collectLastValue(underTest.lockScreenState)
+ val secondLastValue = valueSnapshot()
+ val callbackCaptor: ArgumentCaptor<ZenModeController.Callback> = argumentCaptor()
+ verify(zenModeController).addCallback(callbackCaptor.capture())
- //when
- callbackCaptor.value.onZenChanged(ZEN_MODE_IMPORTANT_INTERRUPTIONS)
- val lastValue = valueSnapshot()
+ // when
+ callbackCaptor.value.onZenChanged(ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ val lastValue = valueSnapshot()
- //then
- assertEquals(
- KeyguardQuickAffordanceConfig.LockScreenState.Visible(
- Icon.Resource(
- R.drawable.qs_dnd_icon_off,
- ContentDescription.Resource(R.string.dnd_is_off)
+ // then
+ assertEquals(
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.qs_dnd_icon_off,
+ ContentDescription.Resource(R.string.dnd_is_off)
+ ),
+ ActivationState.Inactive
),
- ActivationState.Inactive
- ),
- secondLastValue,
- )
- assertEquals(
- KeyguardQuickAffordanceConfig.LockScreenState.Visible(
- Icon.Resource(
- R.drawable.qs_dnd_icon_on,
- ContentDescription.Resource(R.string.dnd_is_on)
+ secondLastValue,
+ )
+ assertEquals(
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.qs_dnd_icon_on,
+ ContentDescription.Resource(R.string.dnd_is_on)
+ ),
+ ActivationState.Active
),
- ActivationState.Active
- ),
- lastValue,
- )
- }
-}
\ No newline at end of file
+ lastValue,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..a3740d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.LiveData
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MuteQuickAffordanceConfigTest : SysuiTestCase() {
+
+ private lateinit var underTest: MuteQuickAffordanceConfig
+ @Mock
+ private lateinit var ringerModeTracker: RingerModeTracker
+ @Mock
+ private lateinit var audioManager: AudioManager
+ @Mock
+ private lateinit var userTracker: UserTracker
+ @Mock
+ private lateinit var userFileManager: UserFileManager
+
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ testScope = TestScope()
+
+ whenever(userTracker.userContext).thenReturn(context)
+ whenever(userFileManager.getSharedPreferences(any(), any(), any()))
+ .thenReturn(context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE))
+
+ underTest = MuteQuickAffordanceConfig(
+ context,
+ userTracker,
+ userFileManager,
+ ringerModeTracker,
+ audioManager
+ )
+ }
+
+ @Test
+ fun `picker state - volume fixed - not available`() = testScope.runTest {
+ //given
+ whenever(audioManager.isVolumeFixed).thenReturn(true)
+
+ //when
+ val result = underTest.getPickerScreenState()
+
+ //then
+ assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result)
+ }
+
+ @Test
+ fun `picker state - volume not fixed - available`() = testScope.runTest {
+ //given
+ whenever(audioManager.isVolumeFixed).thenReturn(false)
+
+ //when
+ val result = underTest.getPickerScreenState()
+
+ //then
+ assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result)
+ }
+
+ @Test
+ fun `triggered - state was previously NORMAL - currently SILENT - move to previous state`() {
+ //given
+ val ringerModeCapture = argumentCaptor<Int>()
+ val ringerModeInternal = mock<LiveData<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+ whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+ underTest.onTriggered(null)
+ whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_SILENT)
+
+ //when
+ val result = underTest.onTriggered(null)
+ verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture()
+
+ //then
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value)
+ }
+
+ @Test
+ fun `triggered - state is not SILENT - move to SILENT ringer`() {
+ //given
+ val ringerModeCapture = argumentCaptor<Int>()
+ val ringerModeInternal = mock<LiveData<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+ whenever(ringerModeInternal.value).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+
+ //when
+ val result = underTest.onTriggered(null)
+ verify(audioManager).ringerModeInternal = ringerModeCapture.capture()
+
+ //then
+ assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+ assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
new file mode 100644
index 0000000..26601b6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.data.quickaffordance
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var featureFlags: FeatureFlags
+ @Mock
+ private lateinit var userTracker: UserTracker
+ @Mock
+ private lateinit var ringerModeTracker: RingerModeTracker
+ @Mock
+ private lateinit var userFileManager: UserFileManager
+ @Mock
+ private lateinit var keyguardQuickAffordanceRepository: KeyguardQuickAffordanceRepository
+
+ private lateinit var testScope: TestScope
+
+ private lateinit var underTest: MuteQuickAffordanceCoreStartable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(true)
+
+ val config: KeyguardQuickAffordanceConfig = mock()
+ whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE)
+
+ val emission = MutableStateFlow(mapOf("testQuickAffordanceKey" to listOf(config)))
+ whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+
+ testScope = TestScope()
+
+ underTest = MuteQuickAffordanceCoreStartable(
+ featureFlags,
+ userTracker,
+ ringerModeTracker,
+ userFileManager,
+ keyguardQuickAffordanceRepository,
+ testScope,
+ )
+ }
+
+ @Test
+ fun `feature flag is OFF - do nothing with keyguardQuickAffordanceRepository`() = testScope.runTest {
+ //given
+ whenever(featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)).thenReturn(false)
+
+ //when
+ underTest.start()
+
+ //then
+ verifyZeroInteractions(keyguardQuickAffordanceRepository)
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `feature flag is ON - call to keyguardQuickAffordanceRepository`() = testScope.runTest {
+ //given
+ val ringerModeInternal = mock<MutableLiveData<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+ //when
+ underTest.start()
+ runCurrent()
+
+ //then
+ verify(keyguardQuickAffordanceRepository).selections
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `ringer mode is changed to SILENT - do not save to shared preferences`() = testScope.runTest {
+ //given
+ val ringerModeInternal = mock<MutableLiveData<Int>>()
+ val observerCaptor = argumentCaptor<Observer<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+ //when
+ underTest.start()
+ runCurrent()
+ verify(ringerModeInternal).observeForever(observerCaptor.capture())
+ observerCaptor.value.onChanged(AudioManager.RINGER_MODE_SILENT)
+
+ //then
+ verifyZeroInteractions(userFileManager)
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `ringerModeInternal changes to something not SILENT - is set in sharedpreferences`() = testScope.runTest {
+ //given
+ val newRingerMode = 99
+ val observerCaptor = argumentCaptor<Observer<Int>>()
+ val ringerModeInternal = mock<MutableLiveData<Int>>()
+ val sharedPrefs = context.getSharedPreferences("quick_affordance_mute_ringer_mode_cache_test", Context.MODE_PRIVATE)
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+ whenever(
+ userFileManager.getSharedPreferences(eq("quick_affordance_mute_ringer_mode_cache"), any(), any())
+ ).thenReturn(sharedPrefs)
+
+ //when
+ underTest.start()
+ runCurrent()
+ verify(ringerModeInternal).observeForever(observerCaptor.capture())
+ observerCaptor.value.onChanged(newRingerMode)
+ val result = sharedPrefs.getInt("key_last_non_silent_ringer_mode", -1)
+
+ //then
+ assertEquals(newRingerMode, result)
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `MUTE is in selections - observe ringerModeInternal`() = testScope.runTest {
+ //given
+ val ringerModeInternal = mock<MutableLiveData<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+ //when
+ underTest.start()
+ runCurrent()
+
+ //then
+ verify(ringerModeInternal).observeForever(any())
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `MUTE is in selections 2x - observe ringerModeInternal`() = testScope.runTest {
+ //given
+ val config: KeyguardQuickAffordanceConfig = mock()
+ whenever(config.key).thenReturn(BuiltInKeyguardQuickAffordanceKeys.MUTE)
+ val emission = MutableStateFlow(mapOf("testKey" to listOf(config), "testkey2" to listOf(config)))
+ whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+ val ringerModeInternal = mock<MutableLiveData<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+ //when
+ underTest.start()
+ runCurrent()
+
+ //then
+ verify(ringerModeInternal).observeForever(any())
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `MUTE is not in selections - stop observing ringerModeInternal`() = testScope.runTest {
+ //given
+ val config: KeyguardQuickAffordanceConfig = mock()
+ whenever(config.key).thenReturn("notmutequickaffordance")
+ val emission = MutableStateFlow(mapOf("testKey" to listOf(config)))
+ whenever(keyguardQuickAffordanceRepository.selections).thenReturn(emission)
+ val ringerModeInternal = mock<MutableLiveData<Int>>()
+ whenever(ringerModeTracker.ringerModeInternal).thenReturn(ringerModeInternal)
+
+ //when
+ underTest.start()
+ runCurrent()
+
+ //then
+ verify(ringerModeInternal).removeObserver(any())
+ coroutineContext.cancelChildren()
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 6255980..9d2ddff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -141,7 +141,7 @@
whenever(controller.isAbleToOpenCameraApp).thenReturn(true)
assertThat(underTest.getPickerScreenState())
- .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+ .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index d875dd9..8f56b95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -114,20 +114,8 @@
}
@Test
- fun `affordance - missing icon - model is none`() = runBlockingTest {
- setUpState(hasWalletIcon = false)
- var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
-
- val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
-
- job.cancel()
- }
-
- @Test
fun `affordance - no selected card - model is none`() = runBlockingTest {
- setUpState(hasWalletIcon = false)
+ setUpState(hasSelectedCard = false)
var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
@@ -159,13 +147,13 @@
setUpState()
assertThat(underTest.getPickerScreenState())
- .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+ .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
}
@Test
fun `getPickerScreenState - unavailable`() = runTest {
setUpState(
- isWalletEnabled = false,
+ isWalletServiceAvailable = false,
)
assertThat(underTest.getPickerScreenState())
@@ -173,9 +161,9 @@
}
@Test
- fun `getPickerScreenState - disabled when there is no icon`() = runTest {
+ fun `getPickerScreenState - disabled when the feature is not enabled`() = runTest {
setUpState(
- hasWalletIcon = false,
+ isWalletEnabled = false,
)
assertThat(underTest.getPickerScreenState())
@@ -194,20 +182,16 @@
private fun setUpState(
isWalletEnabled: Boolean = true,
+ isWalletServiceAvailable: Boolean = true,
isWalletQuerySuccessful: Boolean = true,
- hasWalletIcon: Boolean = true,
hasSelectedCard: Boolean = true,
) {
whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled)
val walletClient: QuickAccessWalletClient = mock()
- val icon: Drawable? =
- if (hasWalletIcon) {
- ICON
- } else {
- null
- }
- whenever(walletClient.tileIcon).thenReturn(icon)
+ whenever(walletClient.tileIcon).thenReturn(ICON)
+ whenever(walletClient.isWalletServiceAvailable).thenReturn(isWalletServiceAvailable)
+
whenever(walletController.walletClient).thenReturn(walletClient)
whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..805dcec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.data.quickaffordance
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.camera.CameraIntentsWrapper
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class VideoCameraQuickAffordanceConfigTest : SysuiTestCase() {
+
+ @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
+
+ private lateinit var underTest: VideoCameraQuickAffordanceConfig
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ VideoCameraQuickAffordanceConfig(
+ context = context,
+ cameraIntents = CameraIntentsWrapper(context),
+ activityIntentHelper = activityIntentHelper,
+ userTracker = FakeUserTracker(),
+ )
+ }
+
+ @Test
+ fun `lockScreenState - visible when launchable`() = runTest {
+ setLaunchable(true)
+
+ val lockScreenState = collectLastValue(underTest.lockScreenState)
+
+ assertThat(lockScreenState())
+ .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java)
+ }
+
+ @Test
+ fun `lockScreenState - hidden when not launchable`() = runTest {
+ setLaunchable(false)
+
+ val lockScreenState = collectLastValue(underTest.lockScreenState)
+
+ assertThat(lockScreenState())
+ .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ }
+
+ @Test
+ fun `getPickerScreenState - default when launchable`() = runTest {
+ setLaunchable(true)
+
+ assertThat(underTest.getPickerScreenState())
+ .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java)
+ }
+
+ @Test
+ fun `getPickerScreenState - unavailable when not launchable`() = runTest {
+ setLaunchable(false)
+
+ assertThat(underTest.getPickerScreenState())
+ .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+ }
+
+ private fun setLaunchable(isLaunchable: Boolean) {
+ whenever(
+ activityIntentHelper.getTargetActivityInfo(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(
+ if (isLaunchable) {
+ mock()
+ } else {
+ null
+ }
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
new file mode 100644
index 0000000..ddd1049
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class BiometricSettingsRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: BiometricSettingsRepository
+
+ @Mock private lateinit var authController: AuthController
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ private lateinit var userRepository: FakeUserRepository
+
+ private lateinit var testDispatcher: TestDispatcher
+ private lateinit var testScope: TestScope
+ private var testableLooper: TestableLooper? = null
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+ testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
+ userRepository = FakeUserRepository()
+ }
+
+ private suspend fun createBiometricSettingsRepository() {
+ userRepository.setUserInfos(listOf(PRIMARY_USER))
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ underTest =
+ BiometricSettingsRepositoryImpl(
+ context = context,
+ lockPatternUtils = lockPatternUtils,
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ authController = authController,
+ userRepository = userRepository,
+ devicePolicyManager = devicePolicyManager,
+ scope = testScope.backgroundScope,
+ backgroundDispatcher = testDispatcher,
+ looper = testableLooper!!.looper,
+ )
+ }
+
+ @Test
+ fun fingerprintEnrollmentChange() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ val fingerprintEnabledByDevicePolicy = collectLastValue(underTest.isFingerprintEnrolled)
+ runCurrent()
+
+ val captor = argumentCaptor<AuthController.Callback>()
+ verify(authController).addCallback(captor.capture())
+ whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(true)
+ captor.value.onEnrollmentsChanged(
+ BiometricType.UNDER_DISPLAY_FINGERPRINT,
+ PRIMARY_USER_ID,
+ true
+ )
+ assertThat(fingerprintEnabledByDevicePolicy()).isTrue()
+
+ whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(false)
+ captor.value.onEnrollmentsChanged(
+ BiometricType.UNDER_DISPLAY_FINGERPRINT,
+ PRIMARY_USER_ID,
+ false
+ )
+ assertThat(fingerprintEnabledByDevicePolicy()).isFalse()
+ }
+
+ @Test
+ fun strongBiometricAllowedChange() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed)
+ runCurrent()
+
+ val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>()
+ verify(lockPatternUtils).registerStrongAuthTracker(captor.capture())
+
+ captor.value
+ .getStub()
+ .onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
+ testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper
+ assertThat(strongBiometricAllowed()).isTrue()
+
+ captor.value
+ .getStub()
+ .onStrongAuthRequiredChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID)
+ testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper
+ assertThat(strongBiometricAllowed()).isFalse()
+ }
+
+ @Test
+ fun fingerprintDisabledByDpmChange() =
+ testScope.runTest {
+ createBiometricSettingsRepository()
+ val fingerprintEnabledByDevicePolicy =
+ collectLastValue(underTest.isFingerprintEnabledByDevicePolicy)
+ runCurrent()
+
+ whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt()))
+ .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT)
+ broadcastDPMStateChange()
+ assertThat(fingerprintEnabledByDevicePolicy()).isFalse()
+
+ whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())).thenReturn(0)
+ broadcastDPMStateChange()
+ assertThat(fingerprintEnabledByDevicePolicy()).isTrue()
+ }
+
+ private fun broadcastDPMStateChange() {
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(
+ context,
+ Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)
+ )
+ }
+ }
+
+ companion object {
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
new file mode 100644
index 0000000..9203f05
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.data.repository
+
+import android.hardware.biometrics.BiometricSourceType
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback>
+
+ private lateinit var testScope: TestScope
+
+ private lateinit var underTest: DeviceEntryFingerprintAuthRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope()
+
+ underTest =
+ DeviceEntryFingerprintAuthRepositoryImpl(
+ keyguardUpdateMonitor,
+ testScope.backgroundScope,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ verify(keyguardUpdateMonitor).removeCallback(callbackCaptor.value)
+ }
+
+ @Test
+ fun isLockedOut_whenFingerprintLockoutStateChanges_emitsNewValue() =
+ testScope.runTest {
+ val isLockedOutValue = collectLastValue(underTest.isLockedOut)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
+
+ callback.onLockedOutStateChanged(BiometricSourceType.FACE)
+ assertThat(isLockedOutValue()).isFalse()
+
+ callback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT)
+ assertThat(isLockedOutValue()).isTrue()
+
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
+ callback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT)
+ assertThat(isLockedOutValue()).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
index 9970a67..444a2a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
@@ -20,6 +20,7 @@
import com.android.keyguard.ViewMediatorCallback
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import org.junit.Before
@@ -34,6 +35,7 @@
@RunWith(JUnit4::class)
class KeyguardBouncerRepositoryTest : SysuiTestCase() {
+ @Mock private lateinit var systemClock: SystemClock
@Mock private lateinit var viewMediatorCallback: ViewMediatorCallback
@Mock private lateinit var bouncerLogger: TableLogBuffer
lateinit var underTest: KeyguardBouncerRepository
@@ -43,7 +45,12 @@
MockitoAnnotations.initMocks(this)
val testCoroutineScope = TestCoroutineScope()
underTest =
- KeyguardBouncerRepository(viewMediatorCallback, testCoroutineScope, bouncerLogger)
+ KeyguardBouncerRepositoryImpl(
+ viewMediatorCallback,
+ systemClock,
+ testCoroutineScope,
+ bouncerLogger,
+ )
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index b071a02..6099f01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -275,10 +275,10 @@
expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
) {
assertThat(observed).isEqualTo(expected)
- assertThat(underTest.getSelections())
+ assertThat(underTest.getCurrentSelections())
.isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
expected.forEach { (slotId, configs) ->
- assertThat(underTest.getSelections(slotId)).isEqualTo(configs)
+ assertThat(underTest.getCurrentSelections(slotId)).isEqualTo(configs)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index be712f6..f997d18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.common.shared.model.Position
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.doze.DozeHost
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
@@ -38,14 +39,17 @@
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
+import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -68,6 +72,7 @@
@Mock private lateinit var authController: AuthController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
+ @Mock private lateinit var dozeParameters: DozeParameters
private lateinit var underTest: KeyguardRepositoryImpl
@@ -84,6 +89,7 @@
keyguardStateController,
keyguardUpdateMonitor,
dozeTransitionListener,
+ dozeParameters,
authController,
dreamOverlayCallbackController,
)
@@ -170,6 +176,26 @@
}
@Test
+ fun isAodAvailable() = runTest {
+ val flow = underTest.isAodAvailable
+ var isAodAvailable = collectLastValue(flow)
+ runCurrent()
+
+ val callback =
+ withArgCaptor<DozeParameters.Callback> { verify(dozeParameters).addCallback(capture()) }
+
+ whenever(dozeParameters.getAlwaysOn()).thenReturn(false)
+ callback.onAlwaysOnChange()
+ assertThat(isAodAvailable()).isEqualTo(false)
+
+ whenever(dozeParameters.getAlwaysOn()).thenReturn(true)
+ callback.onAlwaysOnChange()
+ assertThat(isAodAvailable()).isEqualTo(true)
+
+ flow.onCompletion { verify(dozeParameters).removeCallback(callback) }
+ }
+
+ @Test
fun isKeyguardOccluded() =
runTest(UnconfinedTestDispatcher()) {
whenever(keyguardStateController.isOccluded).thenReturn(false)
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 5d2f0eb..32cec09 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
@@ -104,7 +104,7 @@
val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
- val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.9))
+ val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.1))
assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
job.cancel()
@@ -168,6 +168,25 @@
assertThat(wtfHandler.failed).isTrue()
}
+ @Test
+ fun `Attempt to manually update transition after CANCELED state throws exception`() {
+ val uuid =
+ underTest.startTransition(
+ TransitionInfo(
+ ownerName = OWNER_NAME,
+ from = AOD,
+ to = LOCKSCREEN,
+ animator = null,
+ )
+ )
+
+ checkNotNull(uuid).let {
+ underTest.updateTransition(it, 0.2f, TransitionState.CANCELED)
+ underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+ }
+ assertThat(wtfHandler.failed).isTrue()
+ }
+
private fun listWithStep(
step: BigDecimal,
start: BigDecimal = BigDecimal.ZERO,
@@ -201,7 +220,10 @@
)
)
fractions.forEachIndexed { index, fraction ->
- assertThat(steps[index + 1])
+ val step = steps[index + 1]
+ val truncatedValue =
+ BigDecimal(step.value.toDouble()).setScale(2, RoundingMode.HALF_UP).toFloat()
+ assertThat(step.copy(value = truncatedValue))
.isEqualTo(
TransitionStep(
from,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
new file mode 100644
index 0000000..4b06905
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.data.repository
+
+import android.app.trust.TrustManager
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.keyguard.logging.TrustRepositoryLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+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.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class TrustRepositoryTest : SysuiTestCase() {
+ @Mock private lateinit var trustManager: TrustManager
+ @Captor private lateinit var listenerCaptor: ArgumentCaptor<TrustManager.TrustListener>
+ private lateinit var userRepository: FakeUserRepository
+ private lateinit var testScope: TestScope
+ private val users = listOf(UserInfo(1, "user 1", 0), UserInfo(2, "user 1", 0))
+
+ private lateinit var underTest: TrustRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope()
+ userRepository = FakeUserRepository()
+ userRepository.setUserInfos(users)
+
+ val logger =
+ TrustRepositoryLogger(
+ LogBuffer("TestBuffer", 1, mock(LogcatEchoTracker::class.java), false)
+ )
+ underTest =
+ TrustRepositoryImpl(testScope.backgroundScope, userRepository, trustManager, logger)
+ }
+
+ @Test
+ fun isCurrentUserTrusted_whenTrustChanges_emitsLatestValue() =
+ testScope.runTest {
+ runCurrent()
+ verify(trustManager).registerTrustListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+
+ val currentUserId = users[0].id
+ userRepository.setSelectedUserInfo(users[0])
+ val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+
+ listener.onTrustChanged(true, false, currentUserId, 0, emptyList())
+ assertThat(isCurrentUserTrusted()).isTrue()
+
+ listener.onTrustChanged(false, false, currentUserId, 0, emptyList())
+
+ assertThat(isCurrentUserTrusted()).isFalse()
+ }
+
+ @Test
+ fun isCurrentUserTrusted_isFalse_byDefault() =
+ testScope.runTest {
+ runCurrent()
+
+ val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+
+ assertThat(isCurrentUserTrusted()).isFalse()
+ }
+
+ @Test
+ fun isCurrentUserTrusted_whenTrustChangesForDifferentUser_noop() =
+ testScope.runTest {
+ runCurrent()
+ verify(trustManager).registerTrustListener(listenerCaptor.capture())
+ userRepository.setSelectedUserInfo(users[0])
+ val listener = listenerCaptor.value
+
+ val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+ // current user is trusted.
+ listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+ // some other user is not trusted.
+ listener.onTrustChanged(false, false, users[1].id, 0, emptyList())
+
+ assertThat(isCurrentUserTrusted()).isTrue()
+ }
+
+ @Test
+ fun isCurrentUserTrusted_whenTrustChangesForCurrentUser_emitsNewValue() =
+ testScope.runTest {
+ runCurrent()
+ verify(trustManager).registerTrustListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+ userRepository.setSelectedUserInfo(users[0])
+
+ val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+ listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+ assertThat(isCurrentUserTrusted()).isTrue()
+
+ listener.onTrustChanged(false, true, users[0].id, 0, emptyList())
+ assertThat(isCurrentUserTrusted()).isFalse()
+ }
+
+ @Test
+ fun isCurrentUserTrusted_whenUserChangesWithoutRecentTrustChange_defaultsToFalse() =
+ testScope.runTest {
+ runCurrent()
+ verify(trustManager).registerTrustListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+ userRepository.setSelectedUserInfo(users[0])
+ listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+
+ val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+ userRepository.setSelectedUserInfo(users[1])
+
+ assertThat(isCurrentUserTrusted()).isFalse()
+ }
+
+ @Test
+ fun isCurrentUserTrusted_trustChangesFirstBeforeUserInfoChanges_emitsCorrectValue() =
+ testScope.runTest {
+ runCurrent()
+ verify(trustManager).registerTrustListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+ val isCurrentUserTrusted = collectLastValue(underTest.isCurrentUserTrusted)
+
+ listener.onTrustChanged(true, true, users[0].id, 0, emptyList())
+ assertThat(isCurrentUserTrusted()).isFalse()
+
+ userRepository.setSelectedUserInfo(users[0])
+
+ assertThat(isCurrentUserTrusted()).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
new file mode 100644
index 0000000..8caf60f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.ViewMediatorCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class AlternateBouncerInteractorTest : SysuiTestCase() {
+ private lateinit var underTest: AlternateBouncerInteractor
+ private lateinit var bouncerRepository: KeyguardBouncerRepository
+ private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+ private lateinit var deviceEntryFingerprintAuthRepository:
+ FakeDeviceEntryFingerprintAuthRepository
+ @Mock private lateinit var systemClock: SystemClock
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var bouncerLogger: TableLogBuffer
+ private lateinit var featureFlags: FakeFeatureFlags
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ bouncerRepository =
+ KeyguardBouncerRepositoryImpl(
+ mock(ViewMediatorCallback::class.java),
+ FakeSystemClock(),
+ TestCoroutineScope(),
+ bouncerLogger,
+ )
+ biometricSettingsRepository = FakeBiometricSettingsRepository()
+ deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+ featureFlags = FakeFeatureFlags().apply { this.set(Flags.MODERN_ALTERNATE_BOUNCER, true) }
+ underTest =
+ AlternateBouncerInteractor(
+ bouncerRepository,
+ biometricSettingsRepository,
+ deviceEntryFingerprintAuthRepository,
+ systemClock,
+ keyguardUpdateMonitor,
+ featureFlags,
+ )
+ }
+
+ @Test
+ fun canShowAlternateBouncerForFingerprint_givenCanShow() {
+ givenCanShowAlternateBouncer()
+ assertTrue(underTest.canShowAlternateBouncerForFingerprint())
+ }
+
+ @Test
+ fun canShowAlternateBouncerForFingerprint_alternateBouncerUIUnavailable() {
+ givenCanShowAlternateBouncer()
+ bouncerRepository.setAlternateBouncerUIAvailable(false)
+
+ assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+ }
+
+ @Test
+ fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() {
+ givenCanShowAlternateBouncer()
+ biometricSettingsRepository.setFingerprintEnrolled(false)
+
+ assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+ }
+
+ @Test
+ fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() {
+ givenCanShowAlternateBouncer()
+ biometricSettingsRepository.setStrongBiometricAllowed(false)
+
+ assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+ }
+
+ @Test
+ fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() {
+ givenCanShowAlternateBouncer()
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(false)
+
+ assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+ }
+
+ @Test
+ fun canShowAlternateBouncerForFingerprint_fingerprintLockedOut() {
+ givenCanShowAlternateBouncer()
+ deviceEntryFingerprintAuthRepository.setLockedOut(true)
+
+ assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+ }
+
+ @Test
+ fun show_whenCanShow() {
+ givenCanShowAlternateBouncer()
+
+ assertTrue(underTest.show())
+ assertTrue(bouncerRepository.isAlternateBouncerVisible.value)
+ }
+
+ @Test
+ fun show_whenCannotShow() {
+ givenCannotShowAlternateBouncer()
+
+ assertFalse(underTest.show())
+ assertFalse(bouncerRepository.isAlternateBouncerVisible.value)
+ }
+
+ @Test
+ fun hide_wasPreviouslyShowing() {
+ bouncerRepository.setAlternateVisible(true)
+
+ assertTrue(underTest.hide())
+ assertFalse(bouncerRepository.isAlternateBouncerVisible.value)
+ }
+
+ @Test
+ fun hide_wasNotPreviouslyShowing() {
+ bouncerRepository.setAlternateVisible(false)
+
+ assertFalse(underTest.hide())
+ assertFalse(bouncerRepository.isAlternateBouncerVisible.value)
+ }
+
+ private fun givenCanShowAlternateBouncer() {
+ bouncerRepository.setAlternateBouncerUIAvailable(true)
+ biometricSettingsRepository.setFingerprintEnrolled(true)
+ biometricSettingsRepository.setStrongBiometricAllowed(true)
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
+ deviceEntryFingerprintAuthRepository.setLockedOut(false)
+ }
+
+ private fun givenCannotShowAlternateBouncer() {
+ biometricSettingsRepository.setFingerprintEnrolled(false)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
new file mode 100644
index 0000000..68d13d3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.domain.interactor
+
+import android.app.StatusBarManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.CommandQueue.Callbacks
+import com.android.systemui.util.mockito.argumentCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.onCompletion
+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.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardInteractorTest : SysuiTestCase() {
+ @Mock private lateinit var commandQueue: CommandQueue
+
+ private lateinit var underTest: KeyguardInteractor
+ private lateinit var repository: FakeKeyguardRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ repository = FakeKeyguardRepository()
+ underTest = KeyguardInteractor(repository, commandQueue)
+ }
+
+ @Test
+ fun onCameraLaunchDetected() = runTest {
+ val flow = underTest.onCameraLaunchDetected
+ var cameraLaunchSource = collectLastValue(flow)
+ runCurrent()
+
+ val captor = argumentCaptor<CommandQueue.Callbacks>()
+ verify(commandQueue).addCallback(captor.capture())
+
+ captor.value.onCameraLaunchGestureDetected(StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE)
+ assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.WIGGLE)
+
+ captor.value.onCameraLaunchGestureDetected(
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP
+ )
+ assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.POWER_DOUBLE_TAP)
+
+ captor.value.onCameraLaunchGestureDetected(
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER
+ )
+ assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.LIFT_TRIGGER)
+
+ captor.value.onCameraLaunchGestureDetected(
+ StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE
+ )
+ assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.QUICK_AFFORDANCE)
+
+ flow.onCompletion { verify(commandQueue).removeCallback(captor.value) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
new file mode 100644
index 0000000..9d60b16
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.domain.interactor
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+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.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardLongPressInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var logger: UiEventLogger
+
+ private lateinit var underTest: KeyguardLongPressInteractor
+
+ private lateinit var testScope: TestScope
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ runBlocking { createUnderTest() }
+ }
+
+ @Test
+ fun isEnabled() =
+ testScope.runTest {
+ val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
+ KeyguardState.values().forEach { keyguardState ->
+ setUpState(
+ keyguardState = keyguardState,
+ )
+
+ if (keyguardState == KeyguardState.LOCKSCREEN) {
+ assertThat(isEnabled()).isTrue()
+ } else {
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `isEnabled - always false when quick settings are visible`() =
+ testScope.runTest {
+ val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
+ KeyguardState.values().forEach { keyguardState ->
+ setUpState(
+ keyguardState = keyguardState,
+ isQuickSettingsVisible = true,
+ )
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+
+ @Test
+ fun `long-pressed - pop-up clicked - starts activity`() =
+ testScope.runTest {
+ val menu = collectLastValue(underTest.menu)
+ runCurrent()
+
+ val x = 100
+ val y = 123
+ underTest.onLongPress(x, y)
+ assertThat(menu()).isNotNull()
+ assertThat(menu()?.position?.x).isEqualTo(x)
+ assertThat(menu()?.position?.y).isEqualTo(y)
+
+ menu()?.onClicked?.invoke()
+
+ assertThat(menu()).isNull()
+ verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean())
+ }
+
+ @Test
+ fun `long-pressed - pop-up dismissed - never starts activity`() =
+ testScope.runTest {
+ val menu = collectLastValue(underTest.menu)
+ runCurrent()
+
+ menu()?.onDismissed?.invoke()
+
+ assertThat(menu()).isNull()
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), any(), anyBoolean())
+ }
+
+ @Suppress("DEPRECATION") // We're okay using ACTION_CLOSE_SYSTEM_DIALOGS on system UI.
+ @Test
+ fun `long pressed - close dialogs broadcast received - popup dismissed`() =
+ testScope.runTest {
+ val menu = collectLastValue(underTest.menu)
+ runCurrent()
+
+ underTest.onLongPress(123, 456)
+ assertThat(menu()).isNotNull()
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver ->
+ broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+ }
+
+ assertThat(menu()).isNull()
+ }
+
+ @Test
+ fun `logs when menu is shown`() =
+ testScope.runTest {
+ collectLastValue(underTest.menu)
+ runCurrent()
+
+ underTest.onLongPress(100, 123)
+
+ verify(logger)
+ .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN)
+ }
+
+ @Test
+ fun `logs when menu is clicked`() =
+ testScope.runTest {
+ val menu = collectLastValue(underTest.menu)
+ runCurrent()
+
+ underTest.onLongPress(100, 123)
+ menu()?.onClicked?.invoke()
+
+ verify(logger)
+ .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED)
+ }
+
+ private suspend fun createUnderTest(
+ isLongPressFeatureEnabled: Boolean = true,
+ isRevampedWppFeatureEnabled: Boolean = true,
+ ) {
+ testScope = TestScope()
+ keyguardRepository = FakeKeyguardRepository()
+ keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+
+ underTest =
+ KeyguardLongPressInteractor(
+ unsafeContext = context,
+ scope = testScope.backgroundScope,
+ transitionInteractor =
+ KeyguardTransitionInteractor(
+ repository = keyguardTransitionRepository,
+ ),
+ repository = keyguardRepository,
+ activityStarter = activityStarter,
+ logger = logger,
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled)
+ set(Flags.REVAMPED_WALLPAPER_UI, isRevampedWppFeatureEnabled)
+ },
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ )
+ setUpState()
+ }
+
+ private suspend fun setUpState(
+ keyguardState: KeyguardState = KeyguardState.LOCKSCREEN,
+ isQuickSettingsVisible: Boolean = false,
+ ) {
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ to = keyguardState,
+ ),
+ )
+ keyguardRepository.setQuickSettingsVisible(isVisible = isQuickSettingsVisible)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 14b7c31..43287b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -44,6 +44,7 @@
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
@@ -216,6 +217,7 @@
@Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
@Mock private lateinit var expandable: Expandable
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -286,7 +288,11 @@
)
underTest =
KeyguardQuickAffordanceInteractor(
- keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
+ keyguardInteractor =
+ KeyguardInteractor(
+ repository = FakeKeyguardRepository(),
+ commandQueue = commandQueue
+ ),
registry =
FakeKeyguardQuickAffordanceRegistry(
mapOf(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 972919a..b75a15d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -46,6 +46,7 @@
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
@@ -75,6 +76,7 @@
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -152,7 +154,8 @@
underTest =
KeyguardQuickAffordanceInteractor(
- keyguardInteractor = KeyguardInteractor(repository = repository),
+ keyguardInteractor =
+ KeyguardInteractor(repository = repository, commandQueue = commandQueue),
registry =
FakeKeyguardQuickAffordanceRegistry(
mapOf(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index d2b7838..702f3763 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -17,12 +17,14 @@
package com.android.systemui.keyguard.domain.interactor
import android.animation.ValueAnimator
+import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Interpolators
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -33,10 +35,12 @@
import com.android.systemui.keyguard.util.KeyguardTransitionRunner
import com.android.systemui.shade.data.repository.FakeShadeRepository
import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -54,6 +58,7 @@
*/
@SmallTest
@RunWith(JUnit4::class)
+@FlakyTest(bugId = 265303901)
class KeyguardTransitionScenariosTest : SysuiTestCase() {
private lateinit var testScope: TestScope
@@ -66,9 +71,14 @@
// Used to verify transition requests for test output
@Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
+ private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor
+ private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor
+ private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor
+ private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor
@Before
fun setUp() {
@@ -85,7 +95,7 @@
fromLockscreenTransitionInteractor =
FromLockscreenTransitionInteractor(
scope = testScope,
- keyguardInteractor = KeyguardInteractor(keyguardRepository),
+ keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
shadeRepository = shadeRepository,
keyguardTransitionRepository = mockTransitionRepository,
keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
@@ -95,11 +105,47 @@
fromDreamingTransitionInteractor =
FromDreamingTransitionInteractor(
scope = testScope,
- keyguardInteractor = KeyguardInteractor(keyguardRepository),
+ keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
keyguardTransitionRepository = mockTransitionRepository,
keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
)
fromDreamingTransitionInteractor.start()
+
+ fromAodTransitionInteractor =
+ FromAodTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromAodTransitionInteractor.start()
+
+ fromGoneTransitionInteractor =
+ FromGoneTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromGoneTransitionInteractor.start()
+
+ fromDozingTransitionInteractor =
+ FromDozingTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromDozingTransitionInteractor.start()
+
+ fromOccludedTransitionInteractor =
+ FromOccludedTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromOccludedTransitionInteractor.start()
}
@Test
@@ -135,7 +181,7 @@
keyguardRepository.setDreamingWithOverlay(false)
// AND occluded has stopped
keyguardRepository.setKeyguardOccluded(false)
- runCurrent()
+ advanceUntilIdle()
val info =
withArgCaptor<TransitionInfo> {
@@ -190,6 +236,406 @@
coroutineContext.cancelChildren()
}
+ @Test
+ fun `OCCLUDED to DOZING`() =
+ testScope.runTest {
+ // GIVEN a device with AOD not available
+ keyguardRepository.setAodAvailable(false)
+ runCurrent()
+
+ // GIVEN a prior transition has run to OCCLUDED
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to sleep
+ keyguardRepository.setWakefulnessModel(startingToSleep())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
+ assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `OCCLUDED to AOD`() =
+ testScope.runTest {
+ // GIVEN a device with AOD available
+ keyguardRepository.setAodAvailable(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to OCCLUDED
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to sleep
+ keyguardRepository.setWakefulnessModel(startingToSleep())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
+ assertThat(info.to).isEqualTo(KeyguardState.AOD)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `LOCKSCREEN to DOZING`() =
+ testScope.runTest {
+ // GIVEN a device with AOD not available
+ keyguardRepository.setAodAvailable(false)
+ runCurrent()
+
+ // GIVEN a prior transition has run to LOCKSCREEN
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to sleep
+ keyguardRepository.setWakefulnessModel(startingToSleep())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `LOCKSCREEN to AOD`() =
+ testScope.runTest {
+ // GIVEN a device with AOD available
+ keyguardRepository.setAodAvailable(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to LOCKSCREEN
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to sleep
+ keyguardRepository.setWakefulnessModel(startingToSleep())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.to).isEqualTo(KeyguardState.AOD)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `DOZING to LOCKSCREEN`() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to DOZING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to wake
+ keyguardRepository.setWakefulnessModel(startingToWake())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `DOZING to GONE`() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to DOZING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN biometrics succeeds with wake and unlock mode
+ keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.to).isEqualTo(KeyguardState.GONE)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `GONE to DOZING`() =
+ testScope.runTest {
+ // GIVEN a device with AOD not available
+ keyguardRepository.setAodAvailable(false)
+ runCurrent()
+
+ // GIVEN a prior transition has run to GONE
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to sleep
+ keyguardRepository.setWakefulnessModel(startingToSleep())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.GONE)
+ assertThat(info.to).isEqualTo(KeyguardState.DOZING)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `GONE to AOD`() =
+ testScope.runTest {
+ // GIVEN a device with AOD available
+ keyguardRepository.setAodAvailable(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to GONE
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to sleep
+ keyguardRepository.setWakefulnessModel(startingToSleep())
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to AOD should occur
+ assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.GONE)
+ assertThat(info.to).isEqualTo(KeyguardState.AOD)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `GONE to LOCKSREEN`() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GONE
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN the keyguard starts to show
+ keyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to AOD should occur
+ assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.GONE)
+ assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun `GONE to DREAMING`() =
+ testScope.runTest {
+ // GIVEN a device that is not dreaming or dozing
+ keyguardRepository.setDreamingWithOverlay(false)
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ runCurrent()
+
+ // GIVEN a prior transition has run to GONE
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ reset(mockTransitionRepository)
+
+ // WHEN the device begins to dream
+ keyguardRepository.setDreamingWithOverlay(true)
+ advanceUntilIdle()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DREAMING should occur
+ assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.GONE)
+ assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
private fun startingToWake() =
WakefulnessModel(
WakefulnessState.STARTING_TO_WAKE,
@@ -197,4 +643,12 @@
WakeSleepReason.OTHER,
WakeSleepReason.OTHER
)
+
+ private fun startingToSleep() =
+ WakefulnessModel(
+ WakefulnessState.STARTING_TO_SLEEP,
+ true,
+ WakeSleepReason.OTHER,
+ WakeSleepReason.OTHER
+ )
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
index db9c4e7..fbfeca9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -19,7 +19,6 @@
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.KeyguardBouncer
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,8 +33,10 @@
private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
@Mock
private lateinit var mPrimaryBouncerExpansionCallback:
- KeyguardBouncer.PrimaryBouncerExpansionCallback
- @Mock private lateinit var keyguardResetCallback: KeyguardBouncer.KeyguardResetCallback
+ PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
+ @Mock
+ private lateinit var keyguardResetCallback:
+ PrimaryBouncerCallbackInteractor.KeyguardResetCallback
@Before
fun setup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
index a6fc13b..c5e0252 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -30,11 +30,11 @@
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.BouncerViewDelegate
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any
@@ -68,13 +68,13 @@
@Mock private lateinit var keyguardBypassController: KeyguardBypassController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
private val mainHandler = FakeHandler(Looper.getMainLooper())
- private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
+ private lateinit var underTest: PrimaryBouncerInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
DejankUtils.setImmediate(true)
- mPrimaryBouncerInteractor =
+ underTest =
PrimaryBouncerInteractor(
repository,
bouncerView,
@@ -94,7 +94,7 @@
@Test
fun testShow_isScrimmed() {
- mPrimaryBouncerInteractor.show(true)
+ underTest.show(true)
verify(repository).setOnScreenTurnedOff(false)
verify(repository).setKeyguardAuthenticated(null)
verify(repository).setPrimaryHide(false)
@@ -124,7 +124,7 @@
@Test
fun testHide() {
- mPrimaryBouncerInteractor.hide()
+ underTest.hide()
verify(falsingCollector).onBouncerHidden()
verify(keyguardStateController).notifyBouncerShowing(false)
verify(repository).setPrimaryShowingSoon(false)
@@ -137,7 +137,7 @@
@Test
fun testExpansion() {
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
- mPrimaryBouncerInteractor.setPanelExpansion(0.6f)
+ underTest.setPanelExpansion(0.6f)
verify(repository).setPanelExpansion(0.6f)
verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
}
@@ -146,7 +146,7 @@
fun testExpansion_fullyShown() {
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_VISIBLE)
+ underTest.setPanelExpansion(EXPANSION_VISIBLE)
verify(falsingCollector).onBouncerShown()
verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
}
@@ -155,7 +155,7 @@
fun testExpansion_fullyHidden() {
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN)
+ underTest.setPanelExpansion(EXPANSION_HIDDEN)
verify(repository).setPrimaryVisible(false)
verify(repository).setPrimaryShow(null)
verify(repository).setPrimaryHide(true)
@@ -167,7 +167,7 @@
@Test
fun testExpansion_startingToHide() {
`when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
- mPrimaryBouncerInteractor.setPanelExpansion(0.1f)
+ underTest.setPanelExpansion(0.1f)
verify(repository).setPrimaryStartingToHide(true)
verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
}
@@ -175,7 +175,7 @@
@Test
fun testShowMessage() {
val argCaptor = ArgumentCaptor.forClass(BouncerShowMessageModel::class.java)
- mPrimaryBouncerInteractor.showMessage("abc", null)
+ underTest.showMessage("abc", null)
verify(repository).setShowMessage(argCaptor.capture())
assertThat(argCaptor.value.message).isEqualTo("abc")
}
@@ -184,62 +184,62 @@
fun testDismissAction() {
val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
val cancelAction = mock(Runnable::class.java)
- mPrimaryBouncerInteractor.setDismissAction(onDismissAction, cancelAction)
+ underTest.setDismissAction(onDismissAction, cancelAction)
verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
}
@Test
fun testUpdateResources() {
- mPrimaryBouncerInteractor.updateResources()
+ underTest.updateResources()
verify(repository).setResourceUpdateRequests(true)
}
@Test
fun testNotifyKeyguardAuthenticated() {
- mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(true)
+ underTest.notifyKeyguardAuthenticated(true)
verify(repository).setKeyguardAuthenticated(true)
}
@Test
fun testNotifyShowedMessage() {
- mPrimaryBouncerInteractor.onMessageShown()
+ underTest.onMessageShown()
verify(repository).setShowMessage(null)
}
@Test
fun testOnScreenTurnedOff() {
- mPrimaryBouncerInteractor.onScreenTurnedOff()
+ underTest.onScreenTurnedOff()
verify(repository).setOnScreenTurnedOff(true)
}
@Test
fun testSetKeyguardPosition() {
- mPrimaryBouncerInteractor.setKeyguardPosition(0f)
+ underTest.setKeyguardPosition(0f)
verify(repository).setKeyguardPosition(0f)
}
@Test
fun testNotifyKeyguardAuthenticatedHandled() {
- mPrimaryBouncerInteractor.notifyKeyguardAuthenticatedHandled()
+ underTest.notifyKeyguardAuthenticatedHandled()
verify(repository).setKeyguardAuthenticated(null)
}
@Test
fun testNotifyUpdatedResources() {
- mPrimaryBouncerInteractor.notifyUpdatedResources()
+ underTest.notifyUpdatedResources()
verify(repository).setResourceUpdateRequests(false)
}
@Test
fun testSetBackButtonEnabled() {
- mPrimaryBouncerInteractor.setBackButtonEnabled(true)
+ underTest.setBackButtonEnabled(true)
verify(repository).setIsBackButtonEnabled(true)
}
@Test
fun testStartDisappearAnimation() {
val runnable = mock(Runnable::class.java)
- mPrimaryBouncerInteractor.startDisappearAnimation(runnable)
+ underTest.startDisappearAnimation(runnable)
verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
}
@@ -248,42 +248,42 @@
`when`(repository.primaryBouncerVisible.value).thenReturn(true)
`when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isTrue()
+ assertThat(underTest.isFullyShowing()).isTrue()
`when`(repository.primaryBouncerVisible.value).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isFalse()
+ assertThat(underTest.isFullyShowing()).isFalse()
}
@Test
fun testIsScrimmed() {
`when`(repository.primaryBouncerScrimmed.value).thenReturn(true)
- assertThat(mPrimaryBouncerInteractor.isScrimmed()).isTrue()
+ assertThat(underTest.isScrimmed()).isTrue()
`when`(repository.primaryBouncerScrimmed.value).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.isScrimmed()).isFalse()
+ assertThat(underTest.isScrimmed()).isFalse()
}
@Test
fun testIsInTransit() {
`when`(repository.primaryBouncerShowingSoon.value).thenReturn(true)
- assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+ assertThat(underTest.isInTransit()).isTrue()
`when`(repository.primaryBouncerShowingSoon.value).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.isInTransit()).isFalse()
+ assertThat(underTest.isInTransit()).isFalse()
`when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
- assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+ assertThat(underTest.isInTransit()).isTrue()
}
@Test
fun testIsAnimatingAway() {
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
- assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isTrue()
+ assertThat(underTest.isAnimatingAway()).isTrue()
`when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
- assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isFalse()
+ assertThat(underTest.isAnimatingAway()).isFalse()
}
@Test
fun testWillDismissWithAction() {
`when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
- assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isTrue()
+ assertThat(underTest.willDismissWithAction()).isTrue()
`when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
- assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isFalse()
+ assertThat(underTest.willDismissWithAction()).isFalse()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
new file mode 100644
index 0000000..ea7bc91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.Looper
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() {
+ private lateinit var repository: FakeKeyguardBouncerRepository
+ @Mock private lateinit var bouncerView: BouncerView
+ @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate
+ @Mock private lateinit var keyguardStateController: KeyguardStateController
+ @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+ @Mock private lateinit var primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
+ @Mock private lateinit var falsingCollector: FalsingCollector
+ @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+ @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ private val mainHandler = FakeHandler(Looper.getMainLooper())
+ private lateinit var underTest: PrimaryBouncerInteractor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ repository = FakeKeyguardBouncerRepository()
+ underTest =
+ PrimaryBouncerInteractor(
+ repository,
+ bouncerView,
+ mainHandler,
+ keyguardStateController,
+ keyguardSecurityModel,
+ primaryBouncerCallbackInteractor,
+ falsingCollector,
+ dismissCallbackRegistry,
+ keyguardBypassController,
+ keyguardUpdateMonitor,
+ )
+ }
+
+ @Test
+ fun notInteractableWhenExpansionIsBelow90Percent() = runTest {
+ val isInteractable = collectLastValue(underTest.isInteractable)
+
+ repository.setPrimaryVisible(true)
+ repository.setPanelExpansion(0.15f)
+
+ assertThat(isInteractable()).isFalse()
+ }
+
+ @Test
+ fun notInteractableWhenExpansionAbove90PercentButNotVisible() = runTest {
+ val isInteractable = collectLastValue(underTest.isInteractable)
+
+ repository.setPrimaryVisible(false)
+ repository.setPanelExpansion(0.05f)
+
+ assertThat(isInteractable()).isFalse()
+ }
+
+ @Test
+ fun isInteractableWhenExpansionAbove90PercentAndVisible() = runTest {
+ var isInteractable = collectLastValue(underTest.isInteractable)
+
+ repository.setPrimaryVisible(true)
+ repository.setPanelExpansion(0.09f)
+
+ assertThat(isInteractable()).isTrue()
+
+ repository.setPanelExpansion(0.12f)
+ assertThat(isInteractable()).isFalse()
+
+ repository.setPanelExpansion(0f)
+ assertThat(isInteractable()).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
new file mode 100644
index 0000000..a5b78b74
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -0,0 +1,174 @@
+/*
+ * 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
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionAnimationFlowTest : SysuiTestCase() {
+ private lateinit var underTest: KeyguardTransitionAnimationFlow
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ underTest =
+ KeyguardTransitionAnimationFlow(
+ 1000.milliseconds,
+ repository.transitions,
+ )
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun zeroDurationThrowsException() = runTest {
+ val flow = underTest.createFlow(duration = 0.milliseconds, onStep = { it })
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun startTimePlusDurationGreaterThanTransitionDurationThrowsException() = runTest {
+ val flow =
+ underTest.createFlow(
+ startTime = 300.milliseconds,
+ duration = 800.milliseconds,
+ onStep = { it }
+ )
+ }
+
+ @Test
+ fun onFinishRunsWhenSpecified() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 100.milliseconds,
+ onStep = { it },
+ onFinish = { 10f },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ assertThat(animationValues()).isEqualTo(10f)
+ }
+
+ @Test
+ fun onCancelRunsWhenSpecified() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 100.milliseconds,
+ onStep = { it },
+ onCancel = { 100f },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED))
+ assertThat(animationValues()).isEqualTo(100f)
+ }
+
+ @Test
+ fun usesStartTime() = runTest {
+ val flow =
+ underTest.createFlow(
+ startTime = 500.milliseconds,
+ duration = 500.milliseconds,
+ onStep = { it },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(animationValues()).isEqualTo(0f)
+
+ // Should not emit a value
+ repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+
+ repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0f)
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.2f)
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.6f)
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1f)
+ }
+
+ @Test
+ fun usesInterpolator() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 1000.milliseconds,
+ interpolator = EMPHASIZED_ACCELERATE,
+ onStep = { it },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f))
+ repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.5f))
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.6f))
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.8f))
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(1f))
+ }
+
+ @Test
+ fun usesOnStepToDoubleValue() = runTest {
+ val flow =
+ underTest.createFlow(
+ duration = 1000.milliseconds,
+ onStep = { it * 2 },
+ )
+ var animationValues = collectLastValue(flow)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertFloat(animationValues(), 0f)
+ repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 0.6f)
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1.2f)
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 1.6f)
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertFloat(animationValues(), 2f)
+ }
+
+ private fun assertFloat(actual: Float?, expected: Float) {
+ assertThat(actual!!).isWithin(0.01f).of(expected)
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "GoneToDreamingTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 5571663..06e397d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -18,19 +18,13 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -46,6 +40,7 @@
class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
private lateinit var underTest: DreamingToLockscreenTransitionViewModel
private lateinit var repository: FakeKeyguardTransitionRepository
+ private lateinit var transitionAnimation: KeyguardTransitionAnimationFlow
@Before
fun setUp() {
@@ -63,32 +58,18 @@
val job =
underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(0.6f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.8f))
repository.sendTransitionStep(step(1f))
- // Only 3 values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- EMPHASIZED_ACCELERATE.getInterpolation(
- animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
job.cancel()
}
@@ -100,16 +81,18 @@
val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
repository.sendTransitionStep(step(0.5f))
+ // ...up to here
repository.sendTransitionStep(step(1f))
// Only two values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(2)
- assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
- assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -121,19 +104,15 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.1f))
- // Should start running here...
repository.sendTransitionStep(step(0.2f))
repository.sendTransitionStep(step(0.3f))
- // ...up to here
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(2)
- assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -147,58 +126,27 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_LOCKSCREEN_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.DREAMING,
to = KeyguardState.LOCKSCREEN,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "DreamingToLockscreenTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
new file mode 100644
index 0000000..14c3b50
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GoneToDreamingTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: GoneToDreamingTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = GoneToDreamingTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeOut() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ repository.sendTransitionStep(step(0.2f))
+ repository.sendTransitionStep(step(0.3f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+
+ // Only three values should be present, since the dream overlay runs for a small
+ // fraction of the overall animation time
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ // And a final reset event on CANCEL
+ repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED))
+
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+ job.cancel()
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "GoneToDreamingTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index a2c2f71..4b04b7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -47,6 +47,7 @@
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.any
@@ -84,6 +85,7 @@
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var launchAnimator: DialogLaunchAnimator
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: KeyguardBottomAreaViewModel
@@ -124,7 +126,8 @@
)
repository = FakeKeyguardRepository()
- val keyguardInteractor = KeyguardInteractor(repository = repository)
+ val keyguardInteractor =
+ KeyguardInteractor(repository = repository, commandQueue = commandQueue)
whenever(userTracker.userHandle).thenReturn(mock())
whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
.thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
@@ -231,7 +234,10 @@
@Test
fun `startButton - in preview mode - visible even when keyguard not showing`() =
testScope.runTest {
- underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START)
+ underTest.enablePreviewMode(
+ initiallySelectedSlotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ shouldHighlightSelectedAffordance = true,
+ )
repository.setKeyguardShowing(false)
val latest = collectLastValue(underTest.startButton)
@@ -260,6 +266,7 @@
icon = icon,
canShowWhileLocked = false,
intent = Intent("action"),
+ isSelected = true,
),
configKey = configKey,
)
@@ -267,6 +274,60 @@
}
@Test
+ fun `endButton - in higlighted preview mode - dimmed when other is selected`() =
+ testScope.runTest {
+ underTest.enablePreviewMode(
+ initiallySelectedSlotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ shouldHighlightSelectedAffordance = true,
+ )
+ repository.setKeyguardShowing(false)
+ val startButton = collectLastValue(underTest.startButton)
+ val endButton = collectLastValue(underTest.endButton)
+
+ val icon: Icon = mock()
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = true,
+ isActivated = true,
+ icon = icon,
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ ),
+ )
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_END,
+ testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = true,
+ isActivated = true,
+ icon = icon,
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ ),
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = endButton(),
+ testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = false,
+ isActivated = true,
+ icon = icon,
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ isDimmed = true,
+ ),
+ configKey = configKey,
+ )
+ }
+
+ @Test
fun `endButton - present - visible model - do nothing on click`() =
testScope.runTest {
repository.setKeyguardShowing(true)
@@ -374,7 +435,10 @@
@Test
fun `alpha - in preview mode - does not change`() =
testScope.runTest {
- underTest.enablePreviewMode(null)
+ underTest.enablePreviewMode(
+ initiallySelectedSlotId = null,
+ shouldHighlightSelectedAffordance = false,
+ )
val value = collectLastValue(underTest.alpha)
assertThat(value()).isEqualTo(1f)
@@ -636,6 +700,8 @@
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable)
assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated)
+ assertThat(viewModel.isSelected).isEqualTo(testConfig.isSelected)
+ assertThat(viewModel.isDimmed).isEqualTo(testConfig.isDimmed)
if (testConfig.isVisible) {
assertThat(viewModel.icon).isEqualTo(testConfig.icon)
viewModel.onClicked.invoke(
@@ -661,6 +727,8 @@
val icon: Icon? = null,
val canShowWhileLocked: Boolean = false,
val intent: Intent? = null,
+ val isSelected: Boolean = false,
+ val isDimmed: Boolean = false,
) {
init {
check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
new file mode 100644
index 0000000..ed31dc3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: LockscreenToDreamingTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = LockscreenToDreamingTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeOut() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ repository.sendTransitionStep(step(0.2f))
+ repository.sendTransitionStep(step(0.3f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+
+ // Only three values should be present, since the dream overlay runs for a small
+ // fraction of the overall animation time
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+ // And a final reset event on FINISHED
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+
+ assertThat(values.size).isEqualTo(6)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+ // Validate finished value
+ assertThat(values[5]).isEqualTo(0f)
+
+ job.cancel()
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ value = value,
+ transitionState = state,
+ ownerName = "LockscreenToDreamingTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
new file mode 100644
index 0000000..458b315
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: LockscreenToOccludedTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = LockscreenToOccludedTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeOut() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ repository.sendTransitionStep(step(0.4f))
+ repository.sendTransitionStep(step(0.7f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+
+ // Only 3 values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ // Should start running here...
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+ // ...up to here
+
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationYIsCanceled() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED))
+
+ assertThat(values.size).isEqualTo(4)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
+
+ // Cancel will reset the translation
+ assertThat(values[3]).isEqualTo(0)
+
+ job.cancel()
+ }
+
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING,
+ ): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ value = value,
+ transitionState = state,
+ ownerName = "LockscreenToOccludedTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index 98d292d..a36214e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -18,16 +18,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
-import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -58,21 +54,19 @@
val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
- repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ repository.sendTransitionStep(step(0.1f))
// Should start running here...
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.4f))
repository.sendTransitionStep(step(0.5f))
- // ...up to here
repository.sendTransitionStep(step(0.6f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.8f))
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of the overall animation time
- assertThat(values.size).isEqualTo(3)
- assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
- assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
- assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@@ -86,58 +80,27 @@
val job =
underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0f))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(4)
- assertThat(values[0])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[1])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[2])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
- assertThat(values[3])
- .isEqualTo(
- -pixels +
- EMPHASIZED_DECELERATE.getInterpolation(
- animValue(1f, LOCKSCREEN_TRANSLATION_Y)
- ) * pixels
- )
+ assertThat(values.size).isEqualTo(5)
+ values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
job.cancel()
}
- private fun animValue(stepValue: Float, params: AnimationParams): Float {
- val totalDuration = TO_LOCKSCREEN_DURATION
- val startValue = (params.startTime / totalDuration).toFloat()
-
- val multiplier = (totalDuration / params.duration).toFloat()
- return (stepValue - startValue) * multiplier
- }
-
- private fun step(value: Float): TransitionStep {
+ private fun step(
+ value: Float,
+ state: TransitionState = TransitionState.RUNNING
+ ): TransitionStep {
return TransitionStep(
from = KeyguardState.OCCLUDED,
to = KeyguardState.LOCKSCREEN,
value = value,
- transitionState = TransitionState.RUNNING,
+ transitionState = state,
ownerName = "OccludedToLockscreenTransitionViewModelTest"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
index c88f84a..54fc493 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
@@ -71,7 +71,7 @@
waitUntilComplete(info.animator!!)
}
- suspend private fun waitUntilComplete(animator: ValueAnimator) {
+ private suspend fun waitUntilComplete(animator: ValueAnimator) {
withContext(Dispatchers.Main) {
val startTime = System.currentTimeMillis()
while (!isTerminated && animator.isRunning()) {
@@ -96,6 +96,6 @@
override fun setFrameDelay(delay: Long) {}
companion object {
- private const val MAX_TEST_DURATION = 100L
+ private const val MAX_TEST_DURATION = 200L
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
new file mode 100644
index 0000000..411b1bd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class TableLogBufferFactoryTest : SysuiTestCase() {
+ private val dumpManager: DumpManager = mock()
+ private val systemClock = FakeSystemClock()
+ private val underTest = TableLogBufferFactory(dumpManager, systemClock)
+
+ @Test
+ fun `create - always creates new instance`() {
+ val b1 = underTest.create(NAME_1, SIZE)
+ val b1_copy = underTest.create(NAME_1, SIZE)
+ val b2 = underTest.create(NAME_2, SIZE)
+ val b2_copy = underTest.create(NAME_2, SIZE)
+
+ assertThat(b1).isNotSameInstanceAs(b1_copy)
+ assertThat(b1).isNotSameInstanceAs(b2)
+ assertThat(b2).isNotSameInstanceAs(b2_copy)
+ }
+
+ @Test
+ fun `getOrCreate - reuses instance`() {
+ val b1 = underTest.getOrCreate(NAME_1, SIZE)
+ val b1_copy = underTest.getOrCreate(NAME_1, SIZE)
+ val b2 = underTest.getOrCreate(NAME_2, SIZE)
+ val b2_copy = underTest.getOrCreate(NAME_2, SIZE)
+
+ assertThat(b1).isSameInstanceAs(b1_copy)
+ assertThat(b2).isSameInstanceAs(b2_copy)
+ assertThat(b1).isNotSameInstanceAs(b2)
+ assertThat(b1_copy).isNotSameInstanceAs(b2_copy)
+ }
+
+ companion object {
+ const val NAME_1 = "name 1"
+ const val NAME_2 = "name 2"
+
+ const val SIZE = 8
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index 4d2d0f0..0a5b124 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
USER_ID, true, APP, null, ARTIST, TITLE, null,
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
- InstanceId.fakeInstanceId(-1), -1);
+ InstanceId.fakeInstanceId(-1), -1, false, null);
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 52b694f..53cc78f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.media.controls.pipeline
import android.app.Notification
+import android.app.Notification.FLAG_NO_CLEAR
import android.app.Notification.MediaStyle
import android.app.PendingIntent
import android.app.smartspace.SmartspaceAction
@@ -133,7 +134,8 @@
private val clock = FakeSystemClock()
@Mock private lateinit var tunerService: TunerService
@Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
- @Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
+ @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
+ @Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
@Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
@@ -183,6 +185,8 @@
)
verify(tunerService)
.addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
+ verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
+ verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
session = MediaSession(context, "MediaDataManagerTestSession")
mediaNotification =
SbnBuilder().run {
@@ -228,6 +232,8 @@
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
+ whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
}
@@ -300,6 +306,60 @@
}
@Test
+ fun testLoadMetadata_withExplicitIndicator() {
+ val metadata =
+ MediaMetadata.Builder().run {
+ putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+ putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+ putLong(
+ MediaConstants.METADATA_KEY_IS_EXPLICIT,
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+ )
+ build()
+ }
+ whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
+ whenever(controller.metadata).thenReturn(metadata)
+
+ mediaDataManager.addListener(listener)
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ assertThat(mediaDataCaptor.value!!.isExplicit).isTrue()
+ }
+
+ @Test
+ fun testOnMetaDataLoaded_withoutExplicitIndicator() {
+ whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
+ whenever(controller.metadata).thenReturn(metadataBuilder.build())
+
+ mediaDataManager.addListener(listener)
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ assertThat(mediaDataCaptor.value!!.isExplicit).isFalse()
+ }
+
+ @Test
fun testOnMetaDataLoaded_callsListener() {
addNotificationAndLoad()
verify(logger)
@@ -491,6 +551,7 @@
mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
+
verify(listener)
.onMediaDataLoaded(
eq(KEY),
@@ -502,9 +563,21 @@
)
val data = mediaDataCaptor.value
assertThat(data.resumption).isFalse()
- val resumableData = data.copy(resumeAction = Runnable {})
- mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
- mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
+
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY_2),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ val data2 = mediaDataCaptor.value
+ assertThat(data2.resumption).isFalse()
+
+ mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+ mediaDataManager.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
reset(listener)
// WHEN the first is removed
mediaDataManager.onNotificationRemoved(KEY)
@@ -571,27 +644,8 @@
build()
}
val currentTime = clock.elapsedRealtime()
- mediaDataManager.addResumptionControls(
- USER_ID,
- desc,
- Runnable {},
- session.sessionToken,
- APP_NAME,
- pendingIntent,
- PACKAGE_NAME
- )
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- // THEN the media data indicates that it is for resumption
- verify(listener)
- .onMediaDataLoaded(
- eq(PACKAGE_NAME),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -603,6 +657,112 @@
}
@Test
+ fun testAddResumptionControls_withExplicitIndicator() {
+ val bundle = Bundle()
+ // WHEN resumption controls are added with explicit indicator
+ bundle.putLong(
+ MediaConstants.METADATA_KEY_IS_EXPLICIT,
+ MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+ )
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(bundle)
+ build()
+ }
+ val currentTime = clock.elapsedRealtime()
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.song).isEqualTo(SESSION_TITLE)
+ assertThat(data.app).isEqualTo(APP_NAME)
+ assertThat(data.actions).hasSize(1)
+ assertThat(data.semanticActions!!.playOrPause).isNotNull()
+ assertThat(data.lastActive).isAtLeast(currentTime)
+ assertThat(data.isExplicit).isTrue()
+ verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+ }
+
+ @Test
+ fun testAddResumptionControls_hasPartialProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added with partial progress
+ val progress = 0.5
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
+ )
+ putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress)
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(progress)
+ }
+
+ @Test
+ fun testAddResumptionControls_hasNotPlayedProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added that have not been played
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
+ )
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(0)
+ }
+
+ @Test
+ fun testAddResumptionControls_hasFullProgress() {
+ whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true)
+
+ // WHEN resumption controls are added with progress info
+ val extras =
+ Bundle().apply {
+ putInt(
+ MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
+ MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
+ )
+ }
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setExtras(extras)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ // THEN the media data includes the progress
+ val data = mediaDataCaptor.value
+ assertThat(data.resumption).isTrue()
+ assertThat(data.resumeProgress).isEqualTo(1)
+ }
+
+ @Test
fun testResumptionDisabled_dismissesResumeControls() {
// WHEN there are resume controls and resumption is switched off
val desc =
@@ -610,26 +770,8 @@
setTitle(SESSION_TITLE)
build()
}
- mediaDataManager.addResumptionControls(
- USER_ID,
- desc,
- Runnable {},
- session.sessionToken,
- APP_NAME,
- pendingIntent,
- PACKAGE_NAME
- )
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- verify(listener)
- .onMediaDataLoaded(
- eq(PACKAGE_NAME),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
+ addResumeControlAndLoad(desc)
+
val data = mediaDataCaptor.value
mediaDataManager.setMediaResumptionEnabled(false)
@@ -1207,11 +1349,10 @@
fun testPlaybackStateChange_keyExists_callsListener() {
// Notification has been added
addNotificationAndLoad()
- verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
// Callback gets an updated state
val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
- callbackCaptor.value.invoke(KEY, state)
+ stateCallbackCaptor.value.invoke(KEY, state)
// Listener is notified of updated state
verify(listener)
@@ -1229,11 +1370,10 @@
@Test
fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
val state = PlaybackState.Builder().build()
- verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
// No media added with this key
- callbackCaptor.value.invoke(KEY, state)
+ stateCallbackCaptor.value.invoke(KEY, state)
verify(listener, never())
.onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
}
@@ -1249,10 +1389,9 @@
// And then get a state update
val state = PlaybackState.Builder().build()
- verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
// Then no changes are made
- callbackCaptor.value.invoke(KEY, state)
+ stateCallbackCaptor.value.invoke(KEY, state)
verify(listener, never())
.onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
}
@@ -1264,8 +1403,7 @@
whenever(controller.playbackState).thenReturn(state)
addNotificationAndLoad()
- verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
- callbackCaptor.value.invoke(KEY, state)
+ stateCallbackCaptor.value.invoke(KEY, state)
verify(listener)
.onMediaDataLoaded(
@@ -1307,8 +1445,7 @@
backgroundExecutor.runAllReady()
foregroundExecutor.runAllReady()
- verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
- callbackCaptor.value.invoke(PACKAGE_NAME, state)
+ stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
verify(listener)
.onMediaDataLoaded(
@@ -1333,8 +1470,7 @@
.build()
addNotificationAndLoad()
- verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
- callbackCaptor.value.invoke(KEY, state)
+ stateCallbackCaptor.value.invoke(KEY, state)
verify(listener)
.onMediaDataLoaded(
@@ -1349,6 +1485,210 @@
assertThat(mediaDataCaptor.value.semanticActions).isNull()
}
+ @Test
+ fun testNoClearNotOngoing_canDismiss() {
+ mediaNotification =
+ SbnBuilder().run {
+ setPkg(PACKAGE_NAME)
+ modifyNotification(context).also {
+ it.setSmallIcon(android.R.drawable.ic_media_pause)
+ it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+ it.setOngoing(false)
+ it.setFlag(FLAG_NO_CLEAR, true)
+ }
+ build()
+ }
+ addNotificationAndLoad()
+ assertThat(mediaDataCaptor.value.isClearable).isTrue()
+ }
+
+ @Test
+ fun testOngoing_cannotDismiss() {
+ mediaNotification =
+ SbnBuilder().run {
+ setPkg(PACKAGE_NAME)
+ modifyNotification(context).also {
+ it.setSmallIcon(android.R.drawable.ic_media_pause)
+ it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+ it.setOngoing(true)
+ }
+ build()
+ }
+ addNotificationAndLoad()
+ assertThat(mediaDataCaptor.value.isClearable).isFalse()
+ }
+
+ @Test
+ fun testRetain_notifPlayer_notifRemoved_setToResume() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+ // When a media control based on notification is added, times out, and then removed
+ addNotificationAndLoad()
+ mediaDataManager.setTimedOut(KEY, timedOut = true)
+ assertThat(mediaDataCaptor.value.active).isFalse()
+ mediaDataManager.onNotificationRemoved(KEY)
+
+ // It is converted to a resume player
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(PACKAGE_NAME),
+ eq(KEY),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ assertThat(mediaDataCaptor.value.resumption).isTrue()
+ assertThat(mediaDataCaptor.value.active).isFalse()
+ verify(logger)
+ .logActiveConvertedToResume(
+ anyInt(),
+ eq(PACKAGE_NAME),
+ eq(mediaDataCaptor.value.instanceId)
+ )
+ }
+
+ @Test
+ fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+ // When a media control based on notification is added and times out
+ addNotificationAndLoad()
+ mediaDataManager.setTimedOut(KEY, timedOut = true)
+ assertThat(mediaDataCaptor.value.active).isFalse()
+
+ // and then the session is destroyed
+ sessionCallbackCaptor.value.invoke(KEY)
+
+ // It remains as a regular player
+ verify(listener, never()).onMediaDataRemoved(eq(KEY))
+ verify(listener, never())
+ .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ }
+
+ @Test
+ fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+ // When a media control based on notification is added and then removed, without timing out
+ addNotificationAndLoad()
+ val data = mediaDataCaptor.value
+ assertThat(data.active).isTrue()
+ mediaDataManager.onNotificationRemoved(KEY)
+
+ // It is fully removed
+ verify(listener).onMediaDataRemoved(eq(KEY))
+ verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+ verify(listener, never())
+ .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ }
+
+ @Test
+ fun testRetain_canResume_removeWhileActive_setToResume() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+
+ // When a media control that supports resumption is added
+ addNotificationAndLoad()
+ val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
+ mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+
+ // And then removed while still active
+ mediaDataManager.onNotificationRemoved(KEY)
+
+ // It is converted to a resume player
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(PACKAGE_NAME),
+ eq(KEY),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ assertThat(mediaDataCaptor.value.resumption).isTrue()
+ assertThat(mediaDataCaptor.value.active).isFalse()
+ verify(logger)
+ .logActiveConvertedToResume(
+ anyInt(),
+ eq(PACKAGE_NAME),
+ eq(mediaDataCaptor.value.instanceId)
+ )
+ }
+
+ @Test
+ fun testRetain_sessionPlayer_notifRemoved_doesNotChange() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+ whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+ addPlaybackStateAction()
+
+ // When a media control with PlaybackState actions is added, times out,
+ // and then the notification is removed
+ addNotificationAndLoad()
+ val data = mediaDataCaptor.value
+ assertThat(data.active).isTrue()
+ mediaDataManager.setTimedOut(KEY, timedOut = true)
+ mediaDataManager.onNotificationRemoved(KEY)
+
+ // It remains as a regular player
+ verify(listener, never()).onMediaDataRemoved(eq(KEY))
+ verify(listener, never())
+ .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ }
+
+ @Test
+ fun testRetain_sessionPlayer_sessionDestroyed_setToResume() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+ whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+ addPlaybackStateAction()
+
+ // When a media control with PlaybackState actions is added, times out,
+ // and then the session is destroyed
+ addNotificationAndLoad()
+ val data = mediaDataCaptor.value
+ assertThat(data.active).isTrue()
+ mediaDataManager.setTimedOut(KEY, timedOut = true)
+ sessionCallbackCaptor.value.invoke(KEY)
+
+ // It is converted to a resume player
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(PACKAGE_NAME),
+ eq(KEY),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ assertThat(mediaDataCaptor.value.resumption).isTrue()
+ assertThat(mediaDataCaptor.value.active).isFalse()
+ verify(logger)
+ .logActiveConvertedToResume(
+ anyInt(),
+ eq(PACKAGE_NAME),
+ eq(mediaDataCaptor.value.instanceId)
+ )
+ }
+
+ @Test
+ fun testRetain_sessionPlayer_destroyedWhileActive_fullyRemoved() {
+ whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true)
+ whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+ addPlaybackStateAction()
+
+ // When a media control using session actions is added, and then the session is destroyed
+ // without timing out first
+ addNotificationAndLoad()
+ val data = mediaDataCaptor.value
+ assertThat(data.active).isTrue()
+ sessionCallbackCaptor.value.invoke(KEY)
+
+ // It is fully removed
+ verify(listener).onMediaDataRemoved(eq(KEY))
+ verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+ verify(listener, never())
+ .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+ }
+
/** Helper function to add a media notification and capture the resulting MediaData */
private fun addNotificationAndLoad() {
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
@@ -1364,4 +1704,37 @@
eq(false)
)
}
+
+ /** Helper function to set up a PlaybackState with action */
+ private fun addPlaybackStateAction() {
+ val stateActions = PlaybackState.ACTION_PLAY_PAUSE
+ val stateBuilder = PlaybackState.Builder().setActions(stateActions)
+ stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f)
+ whenever(controller.playbackState).thenReturn(stateBuilder.build())
+ }
+
+ /** Helper function to add a resumption control and capture the resulting MediaData */
+ private fun addResumeControlAndLoad(desc: MediaDescription) {
+ mediaDataManager.addResumptionControls(
+ USER_ID,
+ desc,
+ Runnable {},
+ session.sessionToken,
+ APP_NAME,
+ pendingIntent,
+ PACKAGE_NAME
+ )
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(PACKAGE_NAME),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
index 344dffa..92bf84c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
@@ -72,6 +72,7 @@
private lateinit var executor: FakeExecutor
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
+ @Mock private lateinit var sessionCallback: (String) -> Unit
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
@Captor
private lateinit var dozingCallbackCaptor:
@@ -99,6 +100,7 @@
)
mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback
+ mediaTimeoutListener.sessionCallback = sessionCallback
// Create a media session and notification for testing.
metadataBuilder =
@@ -284,6 +286,7 @@
verify(mediaController).unregisterCallback(anyObject())
assertThat(executor.numPending()).isEqualTo(0)
verify(logger).logSessionDestroyed(eq(KEY))
+ verify(sessionCallback).invoke(eq(KEY))
}
@Test
@@ -322,6 +325,7 @@
// THEN the controller is unregistered, but the timeout is still scheduled
verify(mediaController).unregisterCallback(anyObject())
assertThat(executor.numPending()).isEqualTo(1)
+ verify(sessionCallback, never()).invoke(eq(KEY))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index 6ca34e0..5e5dc8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -17,8 +17,10 @@
package com.android.systemui.media.controls.ui
import android.app.PendingIntent
+import android.content.res.Configuration
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.util.MathUtils.abs
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
@@ -30,14 +32,12 @@
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.pipeline.MediaDataManager
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -55,6 +55,7 @@
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.floatThat
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -85,7 +86,13 @@
@Mock lateinit var debugLogger: MediaCarouselControllerLogger
@Mock lateinit var mediaViewController: MediaViewController
@Mock lateinit var smartspaceMediaData: SmartspaceMediaData
+ @Mock lateinit var mediaCarousel: MediaScrollView
+ @Mock lateinit var pageIndicator: PageIndicator
+ @Mock lateinit var mediaFlags: MediaFlags
@Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+ @Captor
+ lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener>
+ @Captor lateinit var newConfig: ArgumentCaptor<Configuration>
@Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
private val clock = FakeSystemClock()
@@ -109,8 +116,10 @@
falsingManager,
dumpManager,
logger,
- debugLogger
+ debugLogger,
+ mediaFlags,
)
+ verify(configurationController).addCallback(capture(configListener))
verify(mediaDataManager).addListener(capture(listener))
verify(visualStabilityProvider)
.addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
@@ -642,24 +651,56 @@
@Test
fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
val delta = 0.0001F
- val paginationSquishMiddle =
- TRANSFORM_BEZIER.getInterpolation(
- (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
- )
- val paginationSquishEnd =
- TRANSFORM_BEZIER.getInterpolation(
- (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION
- )
+ mediaCarouselController.mediaCarousel = mediaCarousel
+ mediaCarouselController.pageIndicator = pageIndicator
+ whenever(mediaCarousel.measuredHeight).thenReturn(100)
+ whenever(pageIndicator.translationY).thenReturn(80F)
+ whenever(pageIndicator.height).thenReturn(10)
whenever(mediaHostStatesManager.mediaHostStates)
.thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
whenever(mediaHostState.visible).thenReturn(true)
mediaCarouselController.currentEndLocation = LOCATION_QS
- whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+ whenever(mediaHostState.squishFraction).thenReturn(0.938F)
mediaCarouselController.updatePageIndicatorAlpha()
- assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+ verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta }
- whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+ whenever(mediaHostState.squishFraction).thenReturn(1.0F)
mediaCarouselController.updatePageIndicatorAlpha()
- assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+ verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta }
+ }
+
+ @Ignore("b/253229241")
+ @Test
+ fun testOnConfigChanged_playersAreAddedBack() {
+ listener.value.onMediaDataLoaded(
+ "playing local",
+ null,
+ DATA.copy(
+ active = true,
+ isPlaying = true,
+ playbackLocation = MediaData.PLAYBACK_LOCAL,
+ resumption = false
+ )
+ )
+ listener.value.onMediaDataLoaded(
+ "paused local",
+ null,
+ DATA.copy(
+ active = true,
+ isPlaying = false,
+ playbackLocation = MediaData.PLAYBACK_LOCAL,
+ resumption = false
+ )
+ )
+
+ val playersSize = MediaPlayerData.players().size
+
+ configListener.value.onConfigChanged(capture(newConfig))
+
+ assertEquals(playersSize, MediaPlayerData.players().size)
+ assertEquals(
+ MediaPlayerData.getMediaPlayerIndex("playing local"),
+ mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index b65f5cb..26b9204 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -54,6 +54,7 @@
import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
+import com.android.internal.widget.CachingIconView
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
@@ -81,6 +82,7 @@
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.surfaceeffects.ripple.MultiRippleView
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.FakeExecutor
@@ -154,6 +156,7 @@
@Mock private lateinit var albumView: ImageView
private lateinit var titleText: TextView
private lateinit var artistText: TextView
+ private lateinit var explicitIndicator: CachingIconView
private lateinit var seamless: ViewGroup
private lateinit var seamlessButton: View
@Mock private lateinit var seamlessBackground: RippleDrawable
@@ -201,6 +204,9 @@
@Mock private lateinit var coverContainer1: ViewGroup
@Mock private lateinit var coverContainer2: ViewGroup
@Mock private lateinit var coverContainer3: ViewGroup
+ @Mock private lateinit var recAppIconItem: CachingIconView
+ @Mock private lateinit var recCardTitle: TextView
+ @Mock private lateinit var coverItem: ImageView
private lateinit var coverItem1: ImageView
private lateinit var coverItem2: ImageView
private lateinit var coverItem3: ImageView
@@ -216,14 +222,16 @@
this.set(Flags.UMO_SURFACE_RIPPLE, false)
this.set(Flags.UMO_TURBULENCE_NOISE, false)
this.set(Flags.MEDIA_FALSING_PENALTY, true)
+ this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true)
+ this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
}
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Before
fun setUp() {
- bgExecutor = FakeExecutor(FakeSystemClock())
- mainExecutor = FakeExecutor(FakeSystemClock())
+ bgExecutor = FakeExecutor(clock)
+ mainExecutor = FakeExecutor(clock)
whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
@@ -350,6 +358,7 @@
appIcon = ImageView(context)
titleText = TextView(context)
artistText = TextView(context)
+ explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator }
seamless = FrameLayout(context)
seamless.foreground = seamlessBackground
seamlessButton = View(context)
@@ -396,6 +405,7 @@
whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
whenever(viewHolder.titleText).thenReturn(titleText)
whenever(viewHolder.artistText).thenReturn(artistText)
+ whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator)
whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
whenever(viewHolder.seamless).thenReturn(seamless)
whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
@@ -896,6 +906,17 @@
}
@Test
+ fun bind_resumeState_withProgress() {
+ val progress = 0.5
+ val state = mediaData.copy(resumption = true, resumeProgress = progress)
+
+ player.attachPlayer(viewHolder)
+ player.bindPlayer(state, PACKAGE)
+
+ verify(seekBarViewModel).updateStaticProgress(progress)
+ }
+
+ @Test
fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
@@ -1019,6 +1040,7 @@
@Test
fun bindText() {
+ useRealConstraintSets()
player.attachPlayer(viewHolder)
player.bindPlayer(mediaData, PACKAGE)
@@ -1036,6 +1058,8 @@
handler.onAnimationEnd(mockAnimator)
assertThat(titleText.getText()).isEqualTo(TITLE)
assertThat(artistText.getText()).isEqualTo(ARTIST)
+ assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
+ assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
// Rebinding should not trigger animation
player.bindPlayer(mediaData, PACKAGE)
@@ -1043,6 +1067,36 @@
}
@Test
+ fun bindTextWithExplicitIndicator() {
+ useRealConstraintSets()
+ val mediaDataWitExp = mediaData.copy(isExplicit = true)
+ player.attachPlayer(viewHolder)
+ player.bindPlayer(mediaDataWitExp, PACKAGE)
+
+ // Capture animation handler
+ val captor = argumentCaptor<Animator.AnimatorListener>()
+ verify(mockAnimator, times(2)).addListener(captor.capture())
+ val handler = captor.value
+
+ // Validate text views unchanged but animation started
+ assertThat(titleText.getText()).isEqualTo("")
+ assertThat(artistText.getText()).isEqualTo("")
+ verify(mockAnimator, times(1)).start()
+
+ // Binding only after animator runs
+ handler.onAnimationEnd(mockAnimator)
+ assertThat(titleText.getText()).isEqualTo(TITLE)
+ assertThat(artistText.getText()).isEqualTo(ARTIST)
+ assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE)
+ assertThat(collapsedSet.getVisibility(explicitIndicator.id))
+ .isEqualTo(ConstraintSet.VISIBLE)
+
+ // Rebinding should not trigger animation
+ player.bindPlayer(mediaData, PACKAGE)
+ verify(mockAnimator, times(3)).start()
+ }
+
+ @Test
fun bindTextInterrupted() {
val data0 = mediaData.copy(artist = "ARTIST_0")
val data1 = mediaData.copy(artist = "ARTIST_1")
@@ -2020,6 +2074,51 @@
}
@Test
+ fun bindRecommendation_setAfterExecutors() {
+ fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
+ whenever(recommendationViewHolder.mediaAppIcons)
+ .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+ whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
+ whenever(recommendationViewHolder.mediaCoverItems)
+ .thenReturn(listOf(coverItem, coverItem, coverItem))
+
+ val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bmp)
+ canvas.drawColor(Color.RED)
+ val albumArt = Icon.createWithBitmap(bmp)
+ val data =
+ smartspaceData.copy(
+ recommendations =
+ listOf(
+ SmartspaceAction.Builder("id1", "title1")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build(),
+ SmartspaceAction.Builder("id2", "title2")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build(),
+ SmartspaceAction.Builder("id3", "title3")
+ .setSubtitle("subtitle1")
+ .setIcon(albumArt)
+ .setExtras(Bundle.EMPTY)
+ .build()
+ )
+ )
+
+ player.attachRecommendation(recommendationViewHolder)
+ player.bindRecommendation(data)
+ bgExecutor.runAllReady()
+ mainExecutor.runAllReady()
+
+ verify(recCardTitle).setTextColor(any<Int>())
+ verify(recAppIconItem, times(3)).setImageDrawable(any(Drawable::class.java))
+ verify(coverItem, times(3)).setImageDrawable(any(Drawable::class.java))
+ }
+
+ @Test
fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
val semanticActions =
@@ -2083,6 +2182,27 @@
assertThat(player.mRipplesFinishedListener).isNull()
}
+ @Test
+ fun playTurbulenceNoise_finishesAfterDuration() {
+ fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
+ fakeFeatureFlag.set(Flags.UMO_TURBULENCE_NOISE, true)
+
+ player.attachPlayer(viewHolder)
+
+ mainExecutor.execute {
+ player.mRipplesFinishedListener.onRipplesFinish()
+
+ assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE)
+
+ clock.advanceTime(
+ MediaControlPanel.TURBULENCE_NOISE_PLAY_DURATION +
+ TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS.toLong()
+ )
+
+ assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
+ }
+ }
+
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor {
verify(seekBarViewModel).setScrubbingChangeListener(capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index 920801f..a579518 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
import com.android.systemui.dreams.DreamOverlayStateController
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.dream.MediaDreamComplication
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionStateManager
@@ -76,6 +77,7 @@
@Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
@Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Mock private lateinit var keyguardViewController: KeyguardViewController
+ @Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
@Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
@Captor
@@ -110,6 +112,7 @@
keyguardStateController,
bypassController,
mediaCarouselController,
+ mediaDataManager,
keyguardViewController,
dreamOverlayStateController,
configurationController,
@@ -125,6 +128,7 @@
setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+ whenever(mediaDataManager.hasActiveMedia()).thenReturn(true)
whenever(mediaCarouselController.mediaCarouselScrollHandler)
.thenReturn(mediaCarouselScrollHandler)
val observer = wakefullnessObserver.value
@@ -357,17 +361,31 @@
}
@Test
- fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() {
+ fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() {
goToLockscreen()
enterGuidedTransformation()
whenever(lockHost.visible).thenReturn(false)
whenever(qsHost.visible).thenReturn(true)
whenever(qqsHost.visible).thenReturn(true)
+ whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
}
@Test
+ fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() {
+ // To keep the appearing behavior, we need to be in a guided transition
+ goToLockscreen()
+ enterGuidedTransformation()
+ whenever(lockHost.visible).thenReturn(false)
+ whenever(qsHost.visible).thenReturn(true)
+ whenever(qqsHost.visible).thenReturn(true)
+ whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
+
+ assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
+ }
+
+ @Test
fun testDream() {
goToDream()
setMediaDreamComplicationEnabled(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 35b0eb6..2f7eac2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -22,13 +22,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
-import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.util.animation.MeasurementInput
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.animation.TransitionViewState
@@ -60,9 +54,11 @@
@Mock private lateinit var controlWidgetState: WidgetState
@Mock private lateinit var bgWidgetState: WidgetState
@Mock private lateinit var mediaTitleWidgetState: WidgetState
+ @Mock private lateinit var mediaSubTitleWidgetState: WidgetState
@Mock private lateinit var mediaContainerWidgetState: WidgetState
+ @Mock private lateinit var mediaFlags: MediaFlags
- val delta = 0.0001F
+ val delta = 0.1F
private lateinit var mediaViewController: MediaViewController
@@ -70,16 +66,23 @@
fun setup() {
MockitoAnnotations.initMocks(this)
mediaViewController =
- MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+ MediaViewController(
+ context,
+ configurationController,
+ mediaHostStatesManager,
+ logger,
+ mediaFlags,
+ )
}
@Test
fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
- player.measureState = TransitionViewState().apply {
- this.height = 100
- this.measureHeight = 100
- }
+ player.measureState =
+ TransitionViewState().apply {
+ this.height = 100
+ this.measureHeight = 100
+ }
mediaHostStateHolder.expansion = 1f
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
@@ -128,29 +131,21 @@
R.id.header_artist to detailWidgetState
)
)
-
- val detailSquishMiddle =
- TRANSFORM_BEZIER.getInterpolation(
- (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
- )
- mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
+ whenever(mockCopiedState.measureHeight).thenReturn(200)
+ // detail widgets occupy [90, 100]
+ whenever(detailWidgetState.y).thenReturn(90F)
+ whenever(detailWidgetState.height).thenReturn(10)
+ // control widgets occupy [150, 170]
+ whenever(controlWidgetState.y).thenReturn(150F)
+ whenever(controlWidgetState.height).thenReturn(20)
+ // in current beizer, when the progress reach 0.38, the result will be 0.5
+ mediaViewController.squishViewState(mockViewState, 119F / 200F)
verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
- val detailSquishEnd =
- TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
- mediaViewController.squishViewState(mockViewState, detailSquishEnd)
+ mediaViewController.squishViewState(mockViewState, 150F / 200F)
verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-
- val controlSquishMiddle =
- TRANSFORM_BEZIER.getInterpolation(
- (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
- )
- mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+ mediaViewController.squishViewState(mockViewState, 181.4F / 200F)
verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
- val controlSquishEnd =
- TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
- mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+ mediaViewController.squishViewState(mockViewState, 200F / 200F)
verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
}
@@ -161,36 +156,33 @@
.thenReturn(
mutableMapOf(
R.id.media_title1 to mediaTitleWidgetState,
+ R.id.media_subtitle1 to mediaSubTitleWidgetState,
R.id.media_cover1_container to mediaContainerWidgetState
)
)
+ whenever(mockCopiedState.measureHeight).thenReturn(360)
+ // media container widgets occupy [20, 300]
+ whenever(mediaContainerWidgetState.y).thenReturn(20F)
+ whenever(mediaContainerWidgetState.height).thenReturn(280)
+ // media title widgets occupy [320, 330]
+ whenever(mediaTitleWidgetState.y).thenReturn(320F)
+ whenever(mediaTitleWidgetState.height).thenReturn(10)
+ // media subtitle widgets occupy [340, 350]
+ whenever(mediaSubTitleWidgetState.y).thenReturn(340F)
+ whenever(mediaSubTitleWidgetState.height).thenReturn(10)
- val containerSquishMiddle =
- TRANSFORM_BEZIER.getInterpolation(
- (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
- )
- mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+ // in current beizer, when the progress reach 0.38, the result will be 0.5
+ mediaViewController.squishViewState(mockViewState, 307.6F / 360F)
verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
- val containerSquishEnd =
- TRANSFORM_BEZIER.getInterpolation(
- (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
- )
- mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+ mediaViewController.squishViewState(mockViewState, 320F / 360F)
verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-
- val titleSquishMiddle =
- TRANSFORM_BEZIER.getInterpolation(
- (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
- )
- mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+ // media title and media subtitle are in same widget group, should be calculate together and
+ // have same alpha
+ mediaViewController.squishViewState(mockViewState, 353.8F / 360F)
verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
- val titleSquishEnd =
- TRANSFORM_BEZIER.getInterpolation(
- (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
- )
- mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+ verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+ mediaViewController.squishViewState(mockViewState, 360F / 360F)
verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 7c3c9d2..8fd15c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -29,6 +29,7 @@
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.LinearLayout;
+import android.widget.SeekBar;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.filters.SmallTest;
@@ -43,6 +44,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import java.util.ArrayList;
import java.util.List;
@@ -57,6 +60,7 @@
private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
private static final String TEST_SESSION_NAME = "test_session_name";
+
private static final int TEST_MAX_VOLUME = 20;
private static final int TEST_CURRENT_VOLUME = 10;
@@ -69,6 +73,8 @@
private IconCompat mIconCompat = mock(IconCompat.class);
private View mDialogLaunchView = mock(View.class);
+ @Captor
+ private ArgumentCaptor<SeekBar.OnSeekBarChangeListener> mOnSeekBarChangeListenerCaptor;
private MediaOutputAdapter mMediaOutputAdapter;
private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
private List<MediaDevice> mMediaDevices = new ArrayList<>();
@@ -78,6 +84,7 @@
@Before
public void setUp() {
when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(false);
+ when(mMediaOutputController.isSubStatusSupported()).thenReturn(false);
when(mMediaOutputController.getMediaItemList()).thenReturn(mMediaItems);
when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
@@ -348,6 +355,24 @@
}
@Test
+ public void onBindViewHolder_dragSeekbar_setsVolume() {
+ mOnSeekBarChangeListenerCaptor = ArgumentCaptor.forClass(
+ SeekBar.OnSeekBarChangeListener.class);
+ MediaOutputSeekbar mSpySeekbar = spy(mViewHolder.mSeekBar);
+ mViewHolder.mSeekBar = mSpySeekbar;
+ when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+ when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_MAX_VOLUME);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+ verify(mViewHolder.mSeekBar).setOnSeekBarChangeListener(
+ mOnSeekBarChangeListenerCaptor.capture());
+
+ mOnSeekBarChangeListenerCaptor.getValue().onStopTrackingTouch(mViewHolder.mSeekBar);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+ verify(mMediaOutputController).logInteractionAdjustVolume(mMediaDevice1);
+ }
+
+ @Test
public void onBindViewHolder_bindSelectableDevice_verifyView() {
List<MediaDevice> selectableDevices = new ArrayList<>();
selectableDevices.add(mMediaDevice2);
@@ -404,6 +429,24 @@
}
@Test
+ public void subStatusSupported_onBindViewHolder_bindFailedStateDevice_verifyView() {
+ String deviceStatus = "";
+ when(mMediaOutputController.isSubStatusSupported()).thenReturn(true);
+ when(mMediaDevice2.hasDisabledReason()).thenReturn(true);
+ when(mMediaDevice2.getDisableReason()).thenReturn(-1);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(deviceStatus);
+ assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+ }
+
+ @Test
public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
when(mMediaDevice1.getState()).thenReturn(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 094d69a..9a0bd9e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -243,6 +243,13 @@
}
@Test
+ public void dismissDialog_closesDialogByBroadcastSender() {
+ mMediaOutputBaseDialogImpl.dismissDialog();
+
+ verify(mBroadcastSender).closeSystemDialogs();
+ }
+
+ @Test
public void whenBroadcasting_verifyLeBroadcastServiceCallBackIsRegisteredAndUnregistered() {
when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
mLocalBluetoothLeBroadcast);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index b16a39f..7c36e46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -93,6 +93,11 @@
private static final String TEST_SONG = "test_song";
private static final String TEST_SESSION_ID = "test_session_id";
private static final String TEST_SESSION_NAME = "test_session_name";
+ private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+ private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
+ ActivityLaunchAnimator.Controller.class);
+ private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
+ NearbyMediaDevicesManager.class);
// Mock
private MediaController mMediaController = mock(MediaController.class);
private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
@@ -102,8 +107,6 @@
private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
- private MediaItem mMediaItem1 = mock(MediaItem.class);
- private MediaItem mMediaItem2 = mock(MediaItem.class);
private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class);
private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class);
private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
@@ -113,12 +116,7 @@
private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
- private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private FeatureFlags mFlags = mock(FeatureFlags.class);
- private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
- ActivityLaunchAnimator.Controller.class);
- private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
- NearbyMediaDevicesManager.class);
private View mDialogLaunchView = mock(View.class);
private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class);
@@ -127,7 +125,6 @@
private LocalMediaManager mLocalMediaManager;
private List<MediaController> mMediaControllers = new ArrayList<>();
private List<MediaDevice> mMediaDevices = new ArrayList<>();
- private List<MediaItem> mMediaItemList = new ArrayList<>();
private List<NearbyDevice> mNearbyDevices = new ArrayList<>();
private MediaDescription mMediaDescription;
private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
@@ -149,7 +146,9 @@
Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags);
when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(false);
+ when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(false);
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
+ when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
MediaDescription.Builder builder = new MediaDescription.Builder();
builder.setTitle(TEST_SONG);
@@ -160,16 +159,12 @@
when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
mMediaDevices.add(mMediaDevice1);
mMediaDevices.add(mMediaDevice2);
- when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1));
- when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2));
- mMediaItemList.add(mMediaItem1);
- mMediaItemList.add(mMediaItem2);
when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID);
- when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE);
+ when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR);
when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID);
- when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR);
+ when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE);
mNearbyDevices.add(mNearbyDevice1);
mNearbyDevices.add(mNearbyDevice2);
}
@@ -257,6 +252,13 @@
}
@Test
+ public void tryToLaunchMediaApplication_nullIntent_skip() {
+ mMediaOutputController.tryToLaunchMediaApplication();
+
+ verify(mCb, never()).dismissDialog();
+ }
+
+ @Test
public void onDevicesUpdated_unregistersNearbyDevicesCallback() throws RemoteException {
mMediaOutputController.start(mCb);
@@ -274,8 +276,20 @@
mMediaOutputController.onDevicesUpdated(mNearbyDevices);
mMediaOutputController.onDeviceListUpdate(mMediaDevices);
- verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_CLOSE);
- verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_FAR);
+ verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR);
+ verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE);
+ }
+
+ @Test
+ public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation()
+ throws RemoteException {
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+
+ mMediaOutputController.onDevicesUpdated(mNearbyDevices);
+ mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+ assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID);
}
@Test
@@ -292,6 +306,22 @@
}
@Test
+ public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation()
+ throws RemoteException {
+ when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
+ when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(true);
+ when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+
+ mMediaOutputController.onDevicesUpdated(mNearbyDevices);
+ mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+ verify(mMediaDevice1, never()).setRangeZone(anyInt());
+ verify(mMediaDevice2, never()).setRangeZone(anyInt());
+ }
+
+ @Test
public void advanced_onDeviceListUpdate_verifyDeviceListCallback() {
when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
mMediaOutputController.start(mCb);
@@ -307,6 +337,35 @@
assertThat(devices.containsAll(mMediaDevices)).isTrue();
assertThat(devices.size()).isEqualTo(mMediaDevices.size());
+ assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo(
+ mMediaDevices.size() + 2);
+ verify(mCb).onDeviceListChanged();
+ }
+
+ @Test
+ public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() {
+ when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
+ when(mMediaDevice1.isSuggestedDevice()).thenReturn(true);
+ when(mMediaDevice2.isSuggestedDevice()).thenReturn(false);
+
+ mMediaOutputController.start(mCb);
+ reset(mCb);
+ mMediaOutputController.getMediaItemList().clear();
+ mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+ final List<MediaDevice> devices = new ArrayList<>();
+ int dividerSize = 0;
+ for (MediaItem item : mMediaOutputController.getMediaItemList()) {
+ if (item.getMediaDevice().isPresent()) {
+ devices.add(item.getMediaDevice().get());
+ }
+ if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) {
+ dividerSize++;
+ }
+ }
+
+ assertThat(devices.containsAll(mMediaDevices)).isTrue();
+ assertThat(devices.size()).isEqualTo(mMediaDevices.size());
+ assertThat(dividerSize).isEqualTo(2);
verify(mCb).onDeviceListChanged();
}
@@ -463,6 +522,17 @@
}
@Test
+ public void logInteractionAdjustVolume_triggersFromMetricLogger() {
+ MediaOutputMetricLogger spyMediaOutputMetricLogger = spy(
+ mMediaOutputController.mMetricLogger);
+ mMediaOutputController.mMetricLogger = spyMediaOutputMetricLogger;
+
+ mMediaOutputController.logInteractionAdjustVolume(mMediaDevice1);
+
+ verify(spyMediaOutputMetricLogger).logInteractionAdjustVolume(mMediaDevice1);
+ }
+
+ @Test
public void getSessionVolumeMax_triggersFromLocalMediaManager() {
mMediaOutputController.getSessionVolumeMax();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtilsTest.kt
similarity index 70%
copy from packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtilsTest.kt
index 0e7bf8d..8da1c64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerUtilsTest.kt
@@ -22,7 +22,6 @@
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
@@ -31,16 +30,15 @@
import org.mockito.Mockito.mock
@SmallTest
-class MediaTttLoggerTest : SysuiTestCase() {
+class MediaTttLoggerUtilsTest : SysuiTestCase() {
private lateinit var buffer: LogBuffer
- private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
@Before
- fun setUp () {
- buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
- .create("buffer", 10)
- logger = MediaTttLogger(DEVICE_TYPE_TAG, buffer)
+ fun setUp() {
+ buffer =
+ LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+ .create("buffer", 10)
}
@Test
@@ -49,35 +47,33 @@
val id = "test id"
val packageName = "this.is.a.package"
- logger.logStateChange(stateName, id, packageName)
+ MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, id, packageName)
val actualString = getStringFromBuffer()
- assertThat(actualString).contains(DEVICE_TYPE_TAG)
+ assertThat(actualString).contains(TAG)
assertThat(actualString).contains(stateName)
assertThat(actualString).contains(id)
assertThat(actualString).contains(packageName)
}
@Test
- fun logPackageNotFound_bufferHasPackageName() {
- val packageName = "this.is.a.package"
-
- logger.logPackageNotFound(packageName)
+ fun logStateChangeError_hasState() {
+ MediaTttLoggerUtils.logStateChangeError(buffer, TAG, 3456)
val actualString = getStringFromBuffer()
- assertThat(actualString).contains(packageName)
+ assertThat(actualString).contains(TAG)
+ assertThat(actualString).contains("3456")
}
@Test
- fun logRemovalBypass_bufferHasReasons() {
- val removalReason = "fakeRemovalReason"
- val bypassReason = "fakeBypassReason"
+ fun logPackageNotFound_bufferHasPackageName() {
+ val packageName = "this.is.a.package"
- logger.logRemovalBypass(removalReason, bypassReason)
+ MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
val actualString = getStringFromBuffer()
- assertThat(actualString).contains(removalReason)
- assertThat(actualString).contains(bypassReason)
+ assertThat(actualString).contains(TAG)
+ assertThat(actualString).contains(packageName)
}
private fun getStringFromBuffer(): String {
@@ -87,4 +83,4 @@
}
}
-private const val DEVICE_TYPE_TAG = "TEST TYPE"
+private const val TAG = "TEST TAG"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 561867f..8055b98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -25,7 +25,6 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -41,7 +40,6 @@
private lateinit var appIconFromPackageName: Drawable
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var applicationInfo: ApplicationInfo
- @Mock private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
@Before
fun setUp() {
@@ -67,8 +65,7 @@
@Test
fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
- val iconInfo =
- MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
+ val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null) {}
assertThat(iconInfo.isAppIcon).isFalse()
assertThat(iconInfo.contentDescription.loadContentDescription(context))
@@ -77,8 +74,19 @@
}
@Test
+ fun getIconInfoFromPackageName_nullPackageName_exceptionFnNotTriggered() {
+ var exceptionTriggered = false
+
+ MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null) {
+ exceptionTriggered = true
+ }
+
+ assertThat(exceptionTriggered).isFalse()
+ }
+
+ @Test
fun getIconInfoFromPackageName_invalidPackageName_returnsDefault() {
- val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, "fakePackageName", logger)
+ val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, "fakePackageName") {}
assertThat(iconInfo.isAppIcon).isFalse()
assertThat(iconInfo.contentDescription.loadContentDescription(context))
@@ -87,8 +95,19 @@
}
@Test
+ fun getIconInfoFromPackageName_invalidPackageName_exceptionFnTriggered() {
+ var exceptionTriggered = false
+
+ MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = "fakePackageName") {
+ exceptionTriggered = true
+ }
+
+ assertThat(exceptionTriggered).isTrue()
+ }
+
+ @Test
fun getIconInfoFromPackageName_validPackageName_returnsAppInfo() {
- val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME, logger)
+ val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME) {}
assertThat(iconInfo.isAppIcon).isTrue()
assertThat(iconInfo.icon).isEqualTo(MediaTttIcon.Loaded(appIconFromPackageName))
@@ -96,6 +115,17 @@
}
@Test
+ fun getIconInfoFromPackageName_validPackageName_exceptionFnNotTriggered() {
+ var exceptionTriggered = false
+
+ MediaTttUtils.getIconInfoFromPackageName(context, PACKAGE_NAME) {
+ exceptionTriggered = true
+ }
+
+ assertThat(exceptionTriggered).isFalse()
+ }
+
+ @Test
fun iconInfo_toTintedIcon_loaded() {
val contentDescription = ContentDescription.Loaded("test")
val drawable = context.getDrawable(R.drawable.ic_cake)!!
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index 9c4e849..bd042c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -24,7 +24,6 @@
import android.view.accessibility.AccessibilityManager
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -35,7 +34,7 @@
class FakeMediaTttChipControllerReceiver(
commandQueue: CommandQueue,
context: Context,
- logger: MediaTttLogger<ChipReceiverInfo>,
+ logger: MediaTttReceiverLogger,
windowManager: WindowManager,
mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
@@ -48,6 +47,7 @@
viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
+ rippleController: MediaTttReceiverRippleController,
) :
MediaTttChipControllerReceiver(
commandQueue,
@@ -65,6 +65,7 @@
viewUtil,
wakeLockBuilder,
systemClock,
+ rippleController,
) {
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index cefc742..dba2da7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -36,7 +36,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -68,7 +67,7 @@
@Mock
private lateinit var applicationInfo: ApplicationInfo
@Mock
- private lateinit var logger: MediaTttLogger<ChipReceiverInfo>
+ private lateinit var logger: MediaTttReceiverLogger
@Mock
private lateinit var accessibilityManager: AccessibilityManager
@Mock
@@ -85,6 +84,8 @@
private lateinit var windowManager: WindowManager
@Mock
private lateinit var commandQueue: CommandQueue
+ @Mock
+ private lateinit var rippleController: MediaTttReceiverRippleController
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
private lateinit var uiEventLoggerFake: UiEventLoggerFake
@@ -134,6 +135,7 @@
viewUtil,
fakeWakeLockBuilder,
fakeClock,
+ rippleController,
)
controllerReceiver.start()
@@ -163,6 +165,7 @@
viewUtil,
fakeWakeLockBuilder,
fakeClock,
+ rippleController,
)
controllerReceiver.start()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLoggerTest.kt
similarity index 71%
rename from packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLoggerTest.kt
index 0e7bf8d..95df484 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLoggerTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.media.taptotransfer.common
+package com.android.systemui.media.taptotransfer.receiver
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -22,7 +22,6 @@
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
@@ -31,16 +30,17 @@
import org.mockito.Mockito.mock
@SmallTest
-class MediaTttLoggerTest : SysuiTestCase() {
+class MediaTttReceiverLoggerTest : SysuiTestCase() {
private lateinit var buffer: LogBuffer
- private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
+ private lateinit var logger: MediaTttReceiverLogger
@Before
- fun setUp () {
- buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
- .create("buffer", 10)
- logger = MediaTttLogger(DEVICE_TYPE_TAG, buffer)
+ fun setUp() {
+ buffer =
+ LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+ .create("buffer", 10)
+ logger = MediaTttReceiverLogger(buffer)
}
@Test
@@ -52,13 +52,20 @@
logger.logStateChange(stateName, id, packageName)
val actualString = getStringFromBuffer()
- assertThat(actualString).contains(DEVICE_TYPE_TAG)
assertThat(actualString).contains(stateName)
assertThat(actualString).contains(id)
assertThat(actualString).contains(packageName)
}
@Test
+ fun logStateChangeError_hasState() {
+ logger.logStateChangeError(3456)
+
+ val actualString = getStringFromBuffer()
+ assertThat(actualString).contains("3456")
+ }
+
+ @Test
fun logPackageNotFound_bufferHasPackageName() {
val packageName = "this.is.a.package"
@@ -68,23 +75,9 @@
assertThat(actualString).contains(packageName)
}
- @Test
- fun logRemovalBypass_bufferHasReasons() {
- val removalReason = "fakeRemovalReason"
- val bypassReason = "fakeBypassReason"
-
- logger.logRemovalBypass(removalReason, bypassReason)
-
- val actualString = getStringFromBuffer()
- assertThat(actualString).contains(removalReason)
- assertThat(actualString).contains(bypassReason)
- }
-
private fun getStringFromBuffer(): String {
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
return stringWriter.toString()
}
}
-
-private const val DEVICE_TYPE_TAG = "TEST TYPE"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 4cc12c7..ef10e40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -40,18 +40,21 @@
import com.android.systemui.common.shared.model.Text.Companion.loadText
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
+import com.android.systemui.temporarydisplay.chipbar.ChipbarAnimator
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
-import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
-import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil
import com.android.systemui.util.wakelock.WakeLockFake
@@ -61,6 +64,7 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
+import org.mockito.Mockito.atLeast
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
@@ -86,13 +90,14 @@
@Mock private lateinit var falsingManager: FalsingManager
@Mock private lateinit var falsingCollector: FalsingCollector
@Mock private lateinit var chipbarLogger: ChipbarLogger
- @Mock private lateinit var logger: MediaTttLogger<ChipbarInfo>
+ @Mock private lateinit var logger: MediaTttSenderLogger
@Mock private lateinit var mediaTttFlags: MediaTttFlags
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var powerManager: PowerManager
@Mock private lateinit var viewUtil: ViewUtil
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var vibratorHelper: VibratorHelper
+ @Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
private lateinit var fakeWakeLock: WakeLockFake
private lateinit var chipbarCoordinator: ChipbarCoordinator
@@ -132,7 +137,7 @@
uiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
chipbarCoordinator =
- FakeChipbarCoordinator(
+ ChipbarCoordinator(
context,
chipbarLogger,
windowManager,
@@ -141,8 +146,10 @@
configurationController,
dumpManager,
powerManager,
+ ChipbarAnimator(),
falsingManager,
falsingCollector,
+ swipeHandler,
viewUtil,
vibratorHelper,
fakeWakeLockBuilder,
@@ -155,15 +162,14 @@
chipbarCoordinator,
commandQueue,
context,
+ dumpManager,
logger,
mediaTttFlags,
uiEventLogger,
)
underTest.start()
- val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
- verify(commandQueue).addCallback(callbackCaptor.capture())
- commandQueueCallback = callbackCaptor.value!!
+ setCommandQueueCallback()
}
@Test
@@ -175,6 +181,7 @@
chipbarCoordinator,
commandQueue,
context,
+ dumpManager,
logger,
mediaTttFlags,
uiEventLogger,
@@ -206,6 +213,21 @@
}
@Test
+ fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfoWithBlankDeviceName,
+ null,
+ )
+
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .contains(context.getString(R.string.media_ttt_default_device_type))
+ assertThat(chipbarView.getChipText())
+ .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+ }
+
+ @Test
fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() {
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
@@ -248,6 +270,21 @@
}
@Test
+ fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfoWithBlankDeviceName,
+ null,
+ )
+
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .contains(context.getString(R.string.media_ttt_default_device_type))
+ assertThat(chipbarView.getChipText())
+ .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+ }
+
+ @Test
fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() {
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
@@ -506,6 +543,7 @@
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any())
verify(windowManager).removeView(viewCaptor.value)
+ verify(logger).logStateMapRemoval(eq(DEFAULT_ID), any())
}
@Test
@@ -705,6 +743,99 @@
verify(windowManager, never()).addView(any(), any())
}
+ /** Regression test for b/266217596. */
+ @Test
+ fun toReceiver_triggeredThenFar_thenSucceeded_updatesToSucceeded() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null,
+ )
+
+ // WHEN a FAR command comes in
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null,
+ )
+
+ // THEN it is ignored and the chipbar is stilled displayed
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+ assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+ verify(windowManager, never()).removeView(any())
+
+ // WHEN a SUCCEEDED command comes in
+ val succeededRouteInfo =
+ MediaRoute2Info.Builder(DEFAULT_ID, "Tablet Succeeded")
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ succeededRouteInfo,
+ /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {}
+ },
+ )
+
+ // THEN it is *not* marked as an invalid transition and the chipbar updates to the succeeded
+ // state. (The "invalid transition" would be FAR => SUCCEEDED.)
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(
+ ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText(
+ "Tablet Succeeded"
+ )
+ )
+ assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+ assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+ }
+
+ /** Regression test for b/266217596. */
+ @Test
+ fun toThisDevice_triggeredThenFar_thenSucceeded_updatesToSucceeded() {
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ routeInfo,
+ null,
+ )
+
+ // WHEN a FAR command comes in
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null,
+ )
+
+ // THEN it is ignored and the chipbar is stilled displayed
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+ assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+ verify(windowManager, never()).removeView(any())
+
+ // WHEN a SUCCEEDED command comes in
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {}
+ },
+ )
+
+ // THEN it is *not* marked as an invalid transition and the chipbar updates to the succeeded
+ // state. (The "invalid transition" would be FAR => SUCCEEDED.)
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(
+ ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText(
+ "Tablet Succeeded"
+ )
+ )
+ assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+ assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+ }
+
@Test
fun receivesNewStateFromCommandQueue_isLogged() {
commandQueueCallback.updateMediaTapToTransferSenderDisplay(
@@ -890,6 +1021,341 @@
verify(windowManager).removeView(any())
}
+ @Test
+ fun newState_viewListenerRegistered() {
+ val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+ underTest =
+ MediaTttSenderCoordinator(
+ mockChipbarCoordinator,
+ commandQueue,
+ context,
+ dumpManager,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+ // Re-set the command queue callback since we've created a new [MediaTttSenderCoordinator]
+ // with a new callback.
+ setCommandQueueCallback()
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null,
+ )
+
+ verify(mockChipbarCoordinator).registerListener(any())
+ }
+
+ @Test
+ fun onInfoPermanentlyRemoved_viewListenerUnregistered() {
+ val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+ underTest =
+ MediaTttSenderCoordinator(
+ mockChipbarCoordinator,
+ commandQueue,
+ context,
+ dumpManager,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+ setCommandQueueCallback()
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null,
+ )
+
+ val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+ verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
+
+ // WHEN the listener is notified that the view has been removed
+ listenerCaptor.value.onInfoPermanentlyRemoved(DEFAULT_ID, "reason")
+
+ // THEN the media coordinator unregisters the listener
+ verify(logger).logStateMapRemoval(DEFAULT_ID, "reason")
+ verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
+ }
+
+ @Test
+ fun onInfoPermanentlyRemoved_wrongId_viewListenerNotUnregistered() {
+ val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+ underTest =
+ MediaTttSenderCoordinator(
+ mockChipbarCoordinator,
+ commandQueue,
+ context,
+ dumpManager,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+ setCommandQueueCallback()
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null,
+ )
+
+ val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+ verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
+
+ // WHEN the listener is notified that a different view has been removed
+ listenerCaptor.value.onInfoPermanentlyRemoved("differentViewId", "reason")
+
+ // THEN the media coordinator doesn't unregister the listener
+ verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
+ }
+
+ @Test
+ fun farFromReceiverState_viewListenerUnregistered() {
+ val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+ underTest =
+ MediaTttSenderCoordinator(
+ mockChipbarCoordinator,
+ commandQueue,
+ context,
+ dumpManager,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+ setCommandQueueCallback()
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null,
+ )
+
+ val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+ verify(mockChipbarCoordinator).registerListener(capture(listenerCaptor))
+
+ // WHEN we go to the FAR_FROM_RECEIVER state
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ routeInfo,
+ null
+ )
+
+ // THEN the media coordinator unregisters the listener
+ verify(mockChipbarCoordinator).unregisterListener(listenerCaptor.value)
+ }
+
+ @Test
+ fun statesWithDifferentIds_onInfoPermanentlyRemovedForOneId_viewListenerNotUnregistered() {
+ val mockChipbarCoordinator = mock<ChipbarCoordinator>()
+ underTest =
+ MediaTttSenderCoordinator(
+ mockChipbarCoordinator,
+ commandQueue,
+ context,
+ dumpManager,
+ logger,
+ mediaTttFlags,
+ uiEventLogger,
+ )
+ underTest.start()
+ setCommandQueueCallback()
+
+ // WHEN there are two different media transfers with different IDs
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME)
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build(),
+ null,
+ )
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ MediaRoute2Info.Builder("route2", OTHER_DEVICE_NAME)
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build(),
+ null,
+ )
+
+ val listenerCaptor = argumentCaptor<TemporaryViewDisplayController.Listener>()
+ verify(mockChipbarCoordinator, atLeast(1)).registerListener(capture(listenerCaptor))
+
+ // THEN one of them is removed
+ listenerCaptor.value.onInfoPermanentlyRemoved("route1", "reason")
+
+ // THEN the media coordinator doesn't unregister the listener (since route2 is still active)
+ verify(mockChipbarCoordinator, never()).unregisterListener(listenerCaptor.value)
+ verify(logger).logStateMapRemoval("route1", "reason")
+ }
+
+ /** Regression test for b/266218672. */
+ @Test
+ fun twoIdsDisplayed_oldIdIsFar_viewStillDisplayed() {
+ // WHEN there are two different media transfers with different IDs
+ val route1 =
+ MediaRoute2Info.Builder("route1", OTHER_DEVICE_NAME)
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ route1,
+ null,
+ )
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ MediaRoute2Info.Builder("route2", "Route 2 name")
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build(),
+ null,
+ )
+ val newView = getChipbarView()
+
+ // WHEN there's a FAR event for the earlier one
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+ route1,
+ null,
+ )
+
+ // THEN it's ignored and the more recent one is still displayed
+ assertThat(newView.getChipText())
+ .isEqualTo(
+ ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText("Route 2 name")
+ )
+ }
+
+ /** Regression test for b/266218672. */
+ @Test
+ fun receiverSucceededThenTimedOut_internalStateResetAndCanDisplayAlmostCloseToEnd() {
+ displayReceiverTriggered()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ routeInfo,
+ null,
+ )
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+ verify(windowManager).removeView(any())
+
+ reset(windowManager)
+
+ // WHEN we try to show ALMOST_CLOSE_TO_END
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST,
+ routeInfo,
+ null,
+ )
+
+ // THEN it succeeds
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText())
+ }
+
+ /** Regression test for b/266218672. */
+ @Test
+ fun receiverSucceededThenTimedOut_internalStateResetAndCanDisplayReceiverTriggered() {
+ displayReceiverTriggered()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+ routeInfo,
+ null,
+ )
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+ verify(windowManager).removeView(any())
+
+ reset(windowManager)
+
+ // WHEN we try to show RECEIVER_TRIGGERED
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED,
+ routeInfo,
+ null,
+ )
+
+ // THEN it succeeds
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+ assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+ }
+
+ /** Regression test for b/266218672. */
+ @Test
+ fun toThisDeviceSucceededThenTimedOut_internalStateResetAndCanDisplayAlmostCloseToStart() {
+ displayThisDeviceTriggered()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ null,
+ )
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+ verify(windowManager).removeView(any())
+
+ reset(windowManager)
+
+ // WHEN we try to show ALMOST_CLOSE_TO_START
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+ routeInfo,
+ null,
+ )
+
+ // THEN it succeeds
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+ }
+
+ /** Regression test for b/266218672. */
+ @Test
+ fun toThisDeviceSucceededThenTimedOut_internalStateResetAndCanDisplayThisDeviceTriggered() {
+ displayThisDeviceTriggered()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+ routeInfo,
+ null,
+ )
+
+ fakeClock.advanceTime(TIMEOUT + 1L)
+ verify(windowManager).removeView(any())
+
+ reset(windowManager)
+
+ // WHEN we try to show THIS_DEVICE_TRIGGERED
+ val newRouteInfo =
+ MediaRoute2Info.Builder(DEFAULT_ID, "New Name")
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build()
+ commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+ StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ newRouteInfo,
+ null,
+ )
+
+ // THEN it succeeds
+ val chipbarView = getChipbarView()
+ assertThat(chipbarView.getChipText())
+ .isEqualTo(
+ ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText("New Name")
+ )
+ assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+ }
+
private fun getChipbarView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any())
@@ -907,8 +1373,10 @@
private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
- private fun ChipStateSender.getExpectedStateText(): String? {
- return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
+ private fun ChipStateSender.getExpectedStateText(
+ otherDeviceName: String = OTHER_DEVICE_NAME,
+ ): String? {
+ return this.getChipTextString(context, otherDeviceName).loadText(context)
}
// display receiver triggered state helper method to make sure we start from a valid state
@@ -930,15 +1398,30 @@
null
)
}
+
+ private fun setCommandQueueCallback() {
+ val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+ verify(commandQueue).addCallback(capture(callbackCaptor))
+ commandQueueCallback = callbackCaptor.value
+ reset(commandQueue)
+ }
}
+private const val DEFAULT_ID = "defaultId"
private const val APP_NAME = "Fake app name"
private const val OTHER_DEVICE_NAME = "My Tablet"
+private const val BLANK_DEVICE_NAME = " "
private const val PACKAGE_NAME = "com.android.systemui"
private const val TIMEOUT = 10000
private val routeInfo =
- MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
+ MediaRoute2Info.Builder(DEFAULT_ID, OTHER_DEVICE_NAME)
+ .addFeature("feature")
+ .setClientPackageName(PACKAGE_NAME)
+ .build()
+
+private val routeInfoWithBlankDeviceName =
+ MediaRoute2Info.Builder(DEFAULT_ID, BLANK_DEVICE_NAME)
.addFeature("feature")
.setClientPackageName(PACKAGE_NAME)
.build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLoggerTest.kt
similarity index 62%
copy from packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLoggerTest.kt
index 0e7bf8d..0033757 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLoggerTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.media.taptotransfer.common
+package com.android.systemui.media.taptotransfer.sender
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -22,7 +22,6 @@
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogcatEchoTracker
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
@@ -31,16 +30,17 @@
import org.mockito.Mockito.mock
@SmallTest
-class MediaTttLoggerTest : SysuiTestCase() {
+class MediaTttSenderLoggerTest : SysuiTestCase() {
private lateinit var buffer: LogBuffer
- private lateinit var logger: MediaTttLogger<TemporaryViewInfo>
+ private lateinit var logger: MediaTttSenderLogger
@Before
- fun setUp () {
- buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
- .create("buffer", 10)
- logger = MediaTttLogger(DEVICE_TYPE_TAG, buffer)
+ fun setUp() {
+ buffer =
+ LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+ .create("buffer", 10)
+ logger = MediaTttSenderLogger(buffer)
}
@Test
@@ -52,13 +52,20 @@
logger.logStateChange(stateName, id, packageName)
val actualString = getStringFromBuffer()
- assertThat(actualString).contains(DEVICE_TYPE_TAG)
assertThat(actualString).contains(stateName)
assertThat(actualString).contains(id)
assertThat(actualString).contains(packageName)
}
@Test
+ fun logStateChangeError_hasState() {
+ logger.logStateChangeError(3456)
+
+ val actualString = getStringFromBuffer()
+ assertThat(actualString).contains("3456")
+ }
+
+ @Test
fun logPackageNotFound_bufferHasPackageName() {
val packageName = "this.is.a.package"
@@ -80,11 +87,35 @@
assertThat(actualString).contains(bypassReason)
}
+ @Test
+ fun logStateMap_bufferHasInfo() {
+ val map =
+ mapOf(
+ "123" to ChipStateSender.ALMOST_CLOSE_TO_START_CAST,
+ "456" to ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED,
+ )
+
+ logger.logStateMap(map)
+
+ val actualString = getStringFromBuffer()
+ assertThat(actualString).contains("123")
+ assertThat(actualString).contains(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.name)
+ assertThat(actualString).contains("456")
+ assertThat(actualString).contains(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.name)
+ }
+
+ @Test
+ fun logStateMapRemoval_bufferHasInfo() {
+ logger.logStateMapRemoval("456", "testReason")
+
+ val actualString = getStringFromBuffer()
+ assertThat(actualString).contains("456")
+ assertThat(actualString).contains("testReason")
+ }
+
private fun getStringFromBuffer(): String {
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
return stringWriter.toString()
}
}
-
-private const val DEVICE_TYPE_TAG = "TEST TYPE"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
index 19d2d33..1042ea7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
@@ -1,12 +1,16 @@
package com.android.systemui.mediaprojection.appselector
import android.content.ComponentName
+import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Test
@@ -21,11 +25,17 @@
private val scope = CoroutineScope(Dispatchers.Unconfined)
private val appSelectorComponentName = ComponentName("com.test", "AppSelector")
+ private val hostUserHandle = UserHandle.of(123)
+ private val otherUserHandle = UserHandle.of(456)
+
private val view: MediaProjectionAppSelectorView = mock()
+ private val featureFlags: FeatureFlags = mock()
private val controller = MediaProjectionAppSelectorController(
taskListProvider,
view,
+ featureFlags,
+ hostUserHandle,
scope,
appSelectorComponentName
)
@@ -98,15 +108,72 @@
)
}
+ @Test
+ fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesDisabled_bindsOnlyTasksWithHostProfile() {
+ givenEnterprisePoliciesFeatureFlag(enabled = false)
+
+ val tasks = listOf(
+ createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
+ createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
+ createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+ )
+ taskListProvider.tasks = tasks
+
+ controller.init()
+
+ verify(view).bind(
+ listOf(
+ createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+ )
+ )
+ }
+
+ @Test
+ fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesEnabled_bindsAllTasks() {
+ givenEnterprisePoliciesFeatureFlag(enabled = true)
+
+ val tasks = listOf(
+ createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
+ createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
+ createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+ )
+ taskListProvider.tasks = tasks
+
+ controller.init()
+
+ // TODO(b/233348916) should filter depending on the policies
+ verify(view).bind(
+ listOf(
+ createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
+ createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+ createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
+ createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+ )
+ )
+ }
+
+ private fun givenEnterprisePoliciesFeatureFlag(enabled: Boolean) {
+ whenever(featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
+ .thenReturn(enabled)
+ }
+
private fun createRecentTask(
taskId: Int,
- topActivityComponent: ComponentName? = null
+ topActivityComponent: ComponentName? = null,
+ userId: Int = hostUserHandle.identifier
): RecentTask {
return RecentTask(
taskId = taskId,
topActivityComponent = topActivityComponent,
baseIntentComponent = ComponentName("com", "Test"),
- userId = 0,
+ userId = userId,
colorBackground = 0
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
new file mode 100644
index 0000000..e8b6f9b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
@@ -0,0 +1,703 @@
+/*
+ * 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.mediaprojection.devicepolicy
+
+import android.app.admin.DevicePolicyManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.ArgumentMatchers.any
+
+abstract class BaseScreenCaptureDevicePolicyResolverTest(private val precondition: Preconditions) :
+ SysuiTestCase() {
+
+ abstract class Preconditions(
+ val personalScreenCaptureDisabled: Boolean,
+ val workScreenCaptureDisabled: Boolean,
+ val disallowShareIntoManagedProfile: Boolean
+ )
+
+ protected val devicePolicyManager: DevicePolicyManager = mock()
+ protected val userManager: UserManager = mock()
+
+ protected val personalUserHandle: UserHandle = UserHandle.of(123)
+ protected val workUserHandle: UserHandle = UserHandle.of(456)
+
+ protected val policyResolver =
+ ScreenCaptureDevicePolicyResolver(
+ devicePolicyManager,
+ userManager,
+ personalUserHandle,
+ workUserHandle
+ )
+
+ @Before
+ fun setUp() {
+ setUpPolicies()
+ }
+
+ private fun setUpPolicies() {
+ whenever(
+ devicePolicyManager.getScreenCaptureDisabled(
+ any(),
+ eq(personalUserHandle.identifier)
+ )
+ )
+ .thenReturn(precondition.personalScreenCaptureDisabled)
+
+ whenever(devicePolicyManager.getScreenCaptureDisabled(any(), eq(workUserHandle.identifier)))
+ .thenReturn(precondition.workScreenCaptureDisabled)
+
+ whenever(
+ userManager.hasUserRestrictionForUser(
+ eq(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE),
+ eq(workUserHandle)
+ )
+ )
+ .thenReturn(precondition.disallowShareIntoManagedProfile)
+ }
+}
+
+@RunWith(Parameterized::class)
+@SmallTest
+class IsAllowedScreenCaptureDevicePolicyResolverTest(
+ private val test: IsScreenCaptureAllowedTestCase
+) : BaseScreenCaptureDevicePolicyResolverTest(test.given) {
+
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() =
+ listOf(
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false,
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true,
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false,
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true,
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = true,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ IsScreenCaptureAllowedTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ isTargetInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureAllowed = false,
+ ),
+ )
+ }
+
+ class Preconditions(
+ personalScreenCaptureDisabled: Boolean,
+ workScreenCaptureDisabled: Boolean,
+ disallowShareIntoManagedProfile: Boolean,
+ val isHostInWorkProfile: Boolean,
+ val isTargetInWorkProfile: Boolean,
+ ) :
+ BaseScreenCaptureDevicePolicyResolverTest.Preconditions(
+ personalScreenCaptureDisabled,
+ workScreenCaptureDisabled,
+ disallowShareIntoManagedProfile
+ )
+
+ data class IsScreenCaptureAllowedTestCase(
+ val given: Preconditions,
+ val expectedScreenCaptureAllowed: Boolean
+ ) {
+ override fun toString(): String =
+ "isScreenCaptureAllowed: " +
+ "host[${if (given.isHostInWorkProfile) "work" else "personal"} profile], " +
+ "target[${if (given.isTargetInWorkProfile) "work" else "personal"} profile], " +
+ "personal screen capture disabled = ${given.personalScreenCaptureDisabled}, " +
+ "work screen capture disabled = ${given.workScreenCaptureDisabled}, " +
+ "disallow share into managed profile = ${given.disallowShareIntoManagedProfile}, " +
+ "expected screen capture allowed = $expectedScreenCaptureAllowed"
+ }
+
+ @Test
+ fun test() {
+ val targetAppUserHandle =
+ if (test.given.isTargetInWorkProfile) workUserHandle else personalUserHandle
+ val hostAppUserHandle =
+ if (test.given.isHostInWorkProfile) workUserHandle else personalUserHandle
+
+ val screenCaptureAllowed =
+ policyResolver.isScreenCaptureAllowed(targetAppUserHandle, hostAppUserHandle)
+
+ assertWithMessage("Screen capture policy resolved incorrectly")
+ .that(screenCaptureAllowed)
+ .isEqualTo(test.expectedScreenCaptureAllowed)
+ }
+}
+
+@RunWith(Parameterized::class)
+@SmallTest
+class IsCompletelyNotAllowedScreenCaptureDevicePolicyResolverTest(
+ private val test: IsScreenCaptureCompletelyDisabledTestCase
+) : BaseScreenCaptureDevicePolicyResolverTest(test.given) {
+
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() =
+ listOf(
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = false,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = false,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = false,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = false,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = false
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ ),
+ IsScreenCaptureCompletelyDisabledTestCase(
+ given =
+ Preconditions(
+ isHostInWorkProfile = true,
+ personalScreenCaptureDisabled = true,
+ workScreenCaptureDisabled = true,
+ disallowShareIntoManagedProfile = true
+ ),
+ expectedScreenCaptureCompletelyDisabled = true,
+ )
+ )
+ }
+
+ class Preconditions(
+ personalScreenCaptureDisabled: Boolean,
+ workScreenCaptureDisabled: Boolean,
+ disallowShareIntoManagedProfile: Boolean,
+ val isHostInWorkProfile: Boolean,
+ ) :
+ BaseScreenCaptureDevicePolicyResolverTest.Preconditions(
+ personalScreenCaptureDisabled,
+ workScreenCaptureDisabled,
+ disallowShareIntoManagedProfile
+ )
+
+ data class IsScreenCaptureCompletelyDisabledTestCase(
+ val given: Preconditions,
+ val expectedScreenCaptureCompletelyDisabled: Boolean
+ ) {
+ override fun toString(): String =
+ "isScreenCaptureCompletelyDisabled: " +
+ "host[${if (given.isHostInWorkProfile) "work" else "personal"} profile], " +
+ "personal screen capture disabled = ${given.personalScreenCaptureDisabled}, " +
+ "work screen capture disabled = ${given.workScreenCaptureDisabled}, " +
+ "disallow share into managed profile = ${given.disallowShareIntoManagedProfile}, " +
+ "expected screen capture completely disabled = $expectedScreenCaptureCompletelyDisabled"
+ }
+
+ @Test
+ fun test() {
+ val hostAppUserHandle =
+ if (test.given.isHostInWorkProfile) workUserHandle else personalUserHandle
+
+ val completelyDisabled = policyResolver.isScreenCaptureCompletelyDisabled(hostAppUserHandle)
+
+ assertWithMessage("Screen capture policy resolved incorrectly")
+ .that(completelyDisabled)
+ .isEqualTo(test.expectedScreenCaptureCompletelyDisabled)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
index 9bcfd5b..1a93adc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
@@ -26,6 +26,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.FakeDisplayTracker;
import org.junit.Before;
import org.junit.Test;
@@ -46,7 +47,8 @@
@Before
public void setup() {
- mFlagsContainer = new SysUiState();
+ FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext);
+ mFlagsContainer = new SysUiState(displayTracker);
mCallback = mock(SysUiState.SysUiStateCallback.class);
mFlagsContainer.addCallback(mCallback);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
index 1bc4719..1a35502 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -31,10 +31,7 @@
import org.junit.runner.RunWith;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -69,7 +66,7 @@
// Expressive applies hue rotations to the theme color. The input theme color has hue
// 117, ensuring the hue changed significantly is a strong signal styles are being applied.
ColorScheme colorScheme = new ColorScheme(wallpaperColors, false, Style.EXPRESSIVE);
- Assert.assertEquals(357.77, Cam.fromInt(colorScheme.getAccent1().get(6)).getHue(), 0.1);
+ Assert.assertEquals(357.77, Cam.fromInt(colorScheme.getAccent1().getS500()).getHue(), 0.1);
}
@@ -111,7 +108,8 @@
public void testTertiaryHueWrapsProperly() {
int colorInt = 0xffB3588A; // H350 C50 T50
ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */);
- int tertiaryMid = colorScheme.getAccent3().get(colorScheme.getAccent3().size() / 2);
+ int tertiaryMid = colorScheme.getAccent3().getAllShades().get(
+ colorScheme.getShadeCount() / 2);
Cam cam = Cam.fromInt(tertiaryMid);
Assert.assertEquals(cam.getHue(), 50.0, 10.0);
}
@@ -121,7 +119,8 @@
int colorInt = 0xffB3588A; // H350 C50 T50
ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
Style.SPRITZ /* style */);
- int primaryMid = colorScheme.getAccent1().get(colorScheme.getAccent1().size() / 2);
+ int primaryMid = colorScheme.getAccent1().getAllShades().get(
+ colorScheme.getShadeCount() / 2);
Cam cam = Cam.fromInt(primaryMid);
Assert.assertEquals(cam.getChroma(), 12.0, 1.0);
}
@@ -131,7 +130,8 @@
int colorInt = 0xffB3588A; // H350 C50 T50
ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
Style.VIBRANT /* style */);
- int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+ int neutralMid = colorScheme.getNeutral1().getAllShades().get(
+ colorScheme.getShadeCount() / 2);
Cam cam = Cam.fromInt(neutralMid);
Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 12.0);
}
@@ -141,7 +141,8 @@
int colorInt = 0xffB3588A; // H350 C50 T50
ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
Style.EXPRESSIVE /* style */);
- int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+ int neutralMid = colorScheme.getNeutral1().getAllShades().get(
+ colorScheme.getShadeCount() / 2);
Cam cam = Cam.fromInt(neutralMid);
Assert.assertTrue(cam.getChroma() <= 8.0);
}
@@ -151,10 +152,11 @@
int colorInt = 0xffB3588A; // H350 C50 T50
ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
Style.MONOCHROMATIC /* style */);
- int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+ int neutralMid = colorScheme.getNeutral1().getAllShades().get(
+ colorScheme.getShadeCount() / 2);
Assert.assertTrue(
Color.red(neutralMid) == Color.green(neutralMid)
- && Color.green(neutralMid) == Color.blue(neutralMid)
+ && Color.green(neutralMid) == Color.blue(neutralMid)
);
}
@@ -190,15 +192,14 @@
xml.append(" <").append(styleName).append(">");
List<String> colors = new ArrayList<>();
- for (Stream<Integer> stream: Arrays.asList(colorScheme.getAccent1().stream(),
- colorScheme.getAccent2().stream(),
- colorScheme.getAccent3().stream(),
- colorScheme.getNeutral1().stream(),
- colorScheme.getNeutral2().stream())) {
+
+ colorScheme.getAllHues().forEach(schemeHue -> {
colors.add("ffffff");
- colors.addAll(stream.map(Integer::toHexString).map(s -> s.substring(2)).collect(
- Collectors.toList()));
- }
+ schemeHue.getAllShades().forEach(tone -> {
+ colors.add(Integer.toHexString(tone).substring(2));
+ });
+ });
+
xml.append(String.join(",", colors));
xml.append("</").append(styleName).append(">\n");
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java
index 92652a7..3eb7329 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarButtonTest.java
@@ -40,6 +40,7 @@
import com.android.systemui.assist.AssistManager;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.After;
@@ -65,6 +66,7 @@
private ImageReader mReader;
private NavigationBarView mNavBar;
private VirtualDisplay mVirtualDisplay;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
@Before
public void setup() {
@@ -83,6 +85,7 @@
mDependency.injectTestDependency(EdgeBackGestureHandler.Factory.class,
mEdgeBackGestureHandlerFactory);
mNavBar = new NavigationBarView(context, null);
+ mNavBar.setDisplayTracker(mDisplayTracker);
}
private Display createVirtualDisplay() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
index 9bf27a2..aacbf8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
@@ -37,6 +37,7 @@
import static org.mockito.Mockito.when;
import android.content.res.Configuration;
+import android.hardware.display.DisplayManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.util.SparseArray;
@@ -50,12 +51,14 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.model.SysUiState;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.AutoHideController;
import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.util.settings.SecureSettings;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.pip.Pip;
@@ -80,6 +83,7 @@
private NavigationBar mDefaultNavBar;
private NavigationBar mSecondaryNavBar;
private StaticMockitoSession mMockitoSession;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
@Mock
private CommandQueue mCommandQueue;
@@ -91,6 +95,7 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
mNavigationBarController = spy(
new NavigationBarController(mContext,
mock(OverviewProxyService.class),
@@ -102,13 +107,15 @@
mock(NavBarHelper.class),
mTaskbarDelegate,
mNavigationBarFactory,
- mock(StatusBarKeyguardViewManager.class),
mock(DumpManager.class),
mock(AutoHideController.class),
mock(LightBarController.class),
+ TaskStackChangeListeners.getTestInstance(),
Optional.of(mock(Pip.class)),
Optional.of(mock(BackAnimation.class)),
- mock(FeatureFlags.class)));
+ mock(FeatureFlags.class),
+ mock(SecureSettings.class),
+ mDisplayTracker));
initializeNavigationBars();
mMockitoSession = mockitoSession().mockStatic(Utilities.class).startMocking();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 80adbf0..764ddc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -28,6 +28,7 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -44,6 +45,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.display.DisplayManagerGlobal;
@@ -85,11 +87,13 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.recents.Recents;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -203,6 +207,8 @@
private ViewRootImpl mViewRootImpl;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
+ private TaskStackChangeListeners mTaskStackChangeListeners =
+ TaskStackChangeListeners.getTestInstance();
@Rule
public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
@@ -437,6 +443,14 @@
verify(mNavBarHelper, times(1)).getCurrentSysuiState();
}
+ @Test
+ public void testScreenPinningEnabled_updatesSysuiState() {
+ mNavigationBar.init();
+ mTaskStackChangeListeners.getListenerImpl().onLockTaskModeChanged(
+ ActivityManager.LOCK_TASK_MODE_PINNED);
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_SCREEN_PINNING), eq(true));
+ }
+
private NavigationBar createNavBar(Context context) {
DeviceProvisionedController deviceProvisionedController =
mock(DeviceProvisionedController.class);
@@ -481,7 +495,9 @@
mEdgeBackGestureHandler,
Optional.of(mock(BackAnimation.class)),
mUserContextProvider,
- mWakefulnessLifecycle));
+ mWakefulnessLifecycle,
+ mTaskStackChangeListeners,
+ new FakeDisplayTracker(mContext)));
}
private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
index cafd2cf..5270737 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTransitionsTest.java
@@ -36,6 +36,7 @@
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.phone.BarTransitions;
import com.android.systemui.statusbar.phone.LightBarTransitionsController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -63,6 +64,7 @@
IWindowManager mIWindowManager;
private NavigationBarTransitions mTransitions;
+ private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
@Before
public void setup() {
@@ -86,7 +88,7 @@
when(navBar.getCurrentView()).thenReturn(navBar);
when(navBar.findViewById(anyInt())).thenReturn(navBar);
mTransitions = new NavigationBarTransitions(
- navBar, mIWindowManager, mLightBarTransitionsFactory);
+ navBar, mIWindowManager, mLightBarTransitionsFactory, mDisplayTracker);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index 1742c69..537dfb8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -1,11 +1,14 @@
package com.android.systemui.navigationbar
+import android.app.ActivityManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.model.SysUiState
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler
import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.shared.system.QuickStepContract
+import com.android.systemui.shared.system.TaskStackChangeListeners
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.phone.AutoHideController
import com.android.systemui.statusbar.phone.LightBarController
@@ -14,6 +17,7 @@
import com.android.wm.shell.pip.Pip
import org.junit.Before
import org.junit.Test
+import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
@@ -30,6 +34,7 @@
val MODE_GESTURE = 0;
val MODE_THREE_BUTTON = 1;
+ private lateinit var mTaskStackChangeListeners: TaskStackChangeListeners
private lateinit var mTaskbarDelegate: TaskbarDelegate
@Mock
lateinit var mEdgeBackGestureHandlerFactory : EdgeBackGestureHandler.Factory
@@ -69,11 +74,12 @@
`when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController)
`when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState)
`when`(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState)
+ mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
mTaskbarDelegate = TaskbarDelegate(context, mEdgeBackGestureHandlerFactory,
mLightBarControllerFactory)
mTaskbarDelegate.setDependencies(mCommandQueue, mOverviewProxyService, mNavBarHelper,
mNavigationModeController, mSysUiState, mDumpManager, mAutoHideController,
- mLightBarController, mOptionalPip, mBackAnimation)
+ mLightBarController, mOptionalPip, mBackAnimation, mTaskStackChangeListeners)
}
@Test
@@ -90,4 +96,15 @@
mTaskbarDelegate.init(DISPLAY_ID)
verify(mEdgeBackGestureHandler, times(1)).onNavigationModeChanged(MODE_GESTURE)
}
+
+ @Test
+ fun screenPinningEnabled_updatesSysuiState() {
+ mTaskbarDelegate.init(DISPLAY_ID)
+ mTaskStackChangeListeners.listenerImpl.onLockTaskModeChanged(
+ ActivityManager.LOCK_TASK_MODE_PINNED)
+ verify(mSysUiState, times(1)).setFlag(
+ ArgumentMatchers.eq(QuickStepContract.SYSUI_STATE_SCREEN_PINNING),
+ ArgumentMatchers.eq(true)
+ )
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
index 36e02cb..bc67df6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
@@ -16,40 +16,50 @@
internal class FloatingRotationButtonPositionCalculatorTest(private val testCase: TestCase)
: SysuiTestCase() {
- private val calculator = FloatingRotationButtonPositionCalculator(
- MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM
- )
-
@Test
fun calculatePosition() {
- val position = calculator.calculatePosition(
+ val position = testCase.calculator.calculatePosition(
testCase.rotation,
testCase.taskbarVisible,
testCase.taskbarStashed
)
-
assertThat(position).isEqualTo(testCase.expectedPosition)
}
internal class TestCase(
+ val calculator: FloatingRotationButtonPositionCalculator,
val rotation: Int,
val taskbarVisible: Boolean,
val taskbarStashed: Boolean,
val expectedPosition: Position
) {
override fun toString(): String =
- "when rotation = $rotation, " +
+ "when calculator = $calculator, " +
+ "rotation = $rotation, " +
"taskbarVisible = $taskbarVisible, " +
"taskbarStashed = $taskbarStashed - " +
"expected $expectedPosition"
}
companion object {
+ private const val MARGIN_DEFAULT = 10
+ private const val MARGIN_TASKBAR_LEFT = 20
+ private const val MARGIN_TASKBAR_BOTTOM = 30
+
+ private val posLeftCalculator = FloatingRotationButtonPositionCalculator(
+ MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM, true
+ )
+ private val posRightCalculator = FloatingRotationButtonPositionCalculator(
+ MARGIN_DEFAULT, MARGIN_TASKBAR_LEFT, MARGIN_TASKBAR_BOTTOM, false
+ )
+
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun getParams(): Collection<TestCase> =
listOf(
+ // Position left
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_0,
taskbarVisible = false,
taskbarStashed = false,
@@ -60,6 +70,7 @@
)
),
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_90,
taskbarVisible = false,
taskbarStashed = false,
@@ -70,6 +81,7 @@
)
),
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_180,
taskbarVisible = false,
taskbarStashed = false,
@@ -80,6 +92,7 @@
)
),
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_270,
taskbarVisible = false,
taskbarStashed = false,
@@ -90,6 +103,7 @@
)
),
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_0,
taskbarVisible = true,
taskbarStashed = false,
@@ -100,6 +114,7 @@
)
),
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_0,
taskbarVisible = true,
taskbarStashed = true,
@@ -110,6 +125,7 @@
)
),
TestCase(
+ calculator = posLeftCalculator,
rotation = Surface.ROTATION_90,
taskbarVisible = true,
taskbarStashed = false,
@@ -118,11 +134,86 @@
translationX = -MARGIN_TASKBAR_LEFT,
translationY = -MARGIN_TASKBAR_BOTTOM
)
+ ),
+
+ // Position right
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_0,
+ taskbarVisible = false,
+ taskbarStashed = false,
+ expectedPosition = Position(
+ gravity = Gravity.BOTTOM or Gravity.RIGHT,
+ translationX = -MARGIN_DEFAULT,
+ translationY = -MARGIN_DEFAULT
+ )
+ ),
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_90,
+ taskbarVisible = false,
+ taskbarStashed = false,
+ expectedPosition = Position(
+ gravity = Gravity.TOP or Gravity.RIGHT,
+ translationX = -MARGIN_DEFAULT,
+ translationY = MARGIN_DEFAULT
+ )
+ ),
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_180,
+ taskbarVisible = false,
+ taskbarStashed = false,
+ expectedPosition = Position(
+ gravity = Gravity.TOP or Gravity.LEFT,
+ translationX = MARGIN_DEFAULT,
+ translationY = MARGIN_DEFAULT
+ )
+ ),
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_270,
+ taskbarVisible = false,
+ taskbarStashed = false,
+ expectedPosition = Position(
+ gravity = Gravity.BOTTOM or Gravity.LEFT,
+ translationX = MARGIN_DEFAULT,
+ translationY = -MARGIN_DEFAULT
+ )
+ ),
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_0,
+ taskbarVisible = true,
+ taskbarStashed = false,
+ expectedPosition = Position(
+ gravity = Gravity.BOTTOM or Gravity.RIGHT,
+ translationX = -MARGIN_TASKBAR_LEFT,
+ translationY = -MARGIN_TASKBAR_BOTTOM
+ )
+ ),
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_0,
+ taskbarVisible = true,
+ taskbarStashed = true,
+ expectedPosition = Position(
+ gravity = Gravity.BOTTOM or Gravity.RIGHT,
+ translationX = -MARGIN_DEFAULT,
+ translationY = -MARGIN_DEFAULT
+ )
+ ),
+ TestCase(
+ calculator = posRightCalculator,
+ rotation = Surface.ROTATION_90,
+ taskbarVisible = true,
+ taskbarStashed = false,
+ expectedPosition = Position(
+ gravity = Gravity.TOP or Gravity.RIGHT,
+ translationX = -MARGIN_TASKBAR_LEFT,
+ translationY = MARGIN_TASKBAR_BOTTOM
+ )
)
)
-
- private const val MARGIN_DEFAULT = 10
- private const val MARGIN_TASKBAR_LEFT = 20
- private const val MARGIN_TASKBAR_BOTTOM = 30
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 4a9c750..8440455 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -24,7 +24,7 @@
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
@@ -50,7 +50,7 @@
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
- private val notesIntent = Intent(NOTES_ACTION)
+ private val notesIntent = Intent(ACTION_CREATE_NOTE)
@Mock lateinit var context: Context
@Mock lateinit var packageManager: PackageManager
@@ -93,7 +93,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -102,7 +102,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
- verify(bubbles).showAppBubble(notesIntent)
+ verify(bubbles).showOrHideAppBubble(notesIntent)
verify(context, never()).startActivity(notesIntent)
}
@@ -113,7 +113,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = true)
verify(context).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -123,7 +123,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -133,7 +133,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -143,7 +143,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -153,7 +153,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -161,7 +161,7 @@
createNoteTaskController(isEnabled = false).showNoteTask()
verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
@Test
@@ -171,7 +171,7 @@
createNoteTaskController().showNoteTask(isInMultiWindowMode = false)
verify(context, never()).startActivity(notesIntent)
- verify(bubbles, never()).showAppBubble(notesIntent)
+ verify(bubbles, never()).showOrHideAppBubble(notesIntent)
}
// endregion
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 538131a..010ac5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -106,7 +106,9 @@
// region handleSystemKey
@Test
fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() {
- createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+ createNoteTaskInitializer()
+ .callbacks
+ .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT)
verify(noteTaskController).showNoteTask()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
index dd2cc2f..18be92b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
@@ -16,18 +16,14 @@
package com.android.systemui.notetask
-import android.content.ComponentName
+import android.app.role.RoleManager
import android.content.Intent
-import android.content.pm.ActivityInfo
-import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
-import android.content.pm.PackageManager.ResolveInfoFlags
-import android.content.pm.ResolveInfo
-import android.content.pm.ServiceInfo
import android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -48,174 +44,39 @@
internal class NoteTaskIntentResolverTest : SysuiTestCase() {
@Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var roleManager: RoleManager
- private lateinit var resolver: NoteTaskIntentResolver
+ private lateinit var underTest: NoteTaskIntentResolver
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- resolver = NoteTaskIntentResolver(packageManager)
- }
-
- private fun createResolveInfo(
- packageName: String = "PackageName",
- activityInfo: ActivityInfo? = null,
- ): ResolveInfo {
- return ResolveInfo().apply {
- serviceInfo =
- ServiceInfo().apply {
- applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
- }
- this.activityInfo = activityInfo
- }
- }
-
- private fun createActivityInfo(
- name: String? = "ActivityName",
- exported: Boolean = true,
- enabled: Boolean = true,
- showWhenLocked: Boolean = true,
- turnScreenOn: Boolean = true,
- ): ActivityInfo {
- return ActivityInfo().apply {
- this.name = name
- this.exported = exported
- this.enabled = enabled
- if (showWhenLocked) {
- flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED
- }
- if (turnScreenOn) {
- flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
- }
- }
- }
-
- private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) {
- whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>()))
- .thenReturn(block())
- }
-
- private fun givenResolveActivity(block: () -> ResolveInfo?) {
- whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block())
+ underTest = NoteTaskIntentResolver(context, roleManager)
}
@Test
- fun resolveIntent_shouldReturnNotesIntent() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) }
+ fun resolveIntent_shouldReturnIntentInStylusMode() {
+ val packageName = "com.android.note.app"
+ whenever(roleManager.getRoleHoldersAsUser(NoteTaskIntentResolver.ROLE_NOTES, context.user))
+ .then { listOf(packageName) }
- val actual = resolver.resolveIntent()
+ val actual = underTest.resolveIntent()
- val expected =
- Intent(NOTES_ACTION)
- .setComponent(ComponentName("PackageName", "ActivityName"))
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- // Compares the string representation of both intents, as they are different instances.
- assertThat(actual.toString()).isEqualTo(expected.toString())
+ requireNotNull(actual) { "Intent must not be null" }
+ assertThat(actual.action).isEqualTo(ACTION_CREATE_NOTE)
+ assertThat(actual.`package`).isEqualTo(packageName)
+ val expectedExtra = actual.getExtra(NoteTaskIntentResolver.INTENT_EXTRA_USE_STYLUS_MODE)
+ assertThat(expectedExtra).isEqualTo(true)
+ val expectedFlag = actual.flags and Intent.FLAG_ACTIVITY_NEW_TASK
+ assertThat(expectedFlag).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
}
@Test
- fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity {
- createResolveInfo(activityInfo = createActivityInfo(enabled = false))
- }
+ fun resolveIntent_noRoleHolderIsSet_shouldReturnNull() {
+ whenever(roleManager.getRoleHoldersAsUser(eq(NoteTaskIntentResolver.ROLE_NOTES), any()))
+ .then { listOf<String>() }
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity {
- createResolveInfo(activityInfo = createActivityInfo(exported = false))
- }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity {
- createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false))
- }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity {
- createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false))
- }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityInfoIsNull_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity { createResolveInfo(activityInfo = null) }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_resolveActivityIsNull_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo()) }
- givenResolveActivity { null }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
- givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
-
- val actual = resolver.resolveIntent()
-
- assertThat(actual).isNull()
- }
-
- @Test
- fun resolveIntent_activityNotFoundForAction_shouldReturnNull() {
- givenQueryIntentActivities { emptyList() }
-
- val actual = resolver.resolveIntent()
+ val actual = underTest.resolveIntent()
assertThat(actual).isNull()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..a1d42a0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.notetask.quickaffordance
+
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.LockScreenState
+import com.android.systemui.notetask.NoteTaskController
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
+
+/**
+ * Tests for [NoteTaskQuickAffordanceConfig].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskQuickAffordanceConfigTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskQuickAffordanceConfigTest : SysuiTestCase() {
+
+ @Mock lateinit var noteTaskController: NoteTaskController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(noteTaskController.showNoteTask()).then {}
+ }
+
+ private fun createUnderTest(isEnabled: Boolean) =
+ NoteTaskQuickAffordanceConfig(
+ context = context,
+ noteTaskController = noteTaskController,
+ isEnabled = isEnabled,
+ )
+
+ @Test
+ fun lockScreenState_isNotEnabled_shouldEmitHidden() = runTest {
+ val underTest = createUnderTest(isEnabled = false)
+
+ val actual = collectLastValue(underTest.lockScreenState)
+
+ assertThat(actual()).isEqualTo(LockScreenState.Hidden)
+ }
+
+ @Test
+ fun lockScreenState_isEnabled_shouldEmitVisible() = runTest {
+ val stringResult = "Notetaking"
+ val underTest = createUnderTest(isEnabled = true)
+
+ val actual = collectLastValue(underTest.lockScreenState)
+
+ val expected =
+ LockScreenState.Visible(
+ icon =
+ Icon.Resource(
+ res = R.drawable.ic_note_task_shortcut_keyguard,
+ contentDescription = ContentDescription.Loaded(stringResult),
+ )
+ )
+ assertThat(actual()).isEqualTo(expected)
+ }
+
+ @Test
+ fun onTriggered_shouldLaunchNoteTask() {
+ val underTest = createUnderTest(isEnabled = false)
+
+ underTest.onTriggered(expandable = null)
+
+ verify(noteTaskController).showNoteTask()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index a56990f..4a6158f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -30,6 +30,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
@@ -53,6 +54,7 @@
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.NotificationChannels;
@@ -82,6 +84,8 @@
@Mock
private UiEventLogger mUiEventLogger;
@Mock
+ private UserTracker mUserTracker;
+ @Mock
private View mView;
private BroadcastReceiver mReceiver;
@@ -103,8 +107,12 @@
mContext.addMockSystemService(NotificationManager.class, mMockNotificationManager);
ActivityStarter starter = mDependency.injectMockDependency(ActivityStarter.class);
BroadcastSender broadcastSender = mDependency.injectMockDependency(BroadcastSender.class);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+ when(mUserTracker.getUserHandle()).thenReturn(
+ UserHandle.of(ActivityManager.getCurrentUser()));
mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter,
- broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger);
+ broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger,
+ mUserTracker);
BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1,
BatteryManager.BATTERY_HEALTH_GOOD, 5, 15);
mPowerNotificationWarnings.updateSnapshot(snapshot);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java
index 43fb1bd..dee1cc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/AutoAddTrackerTest.java
@@ -59,6 +59,7 @@
@SmallTest
public class AutoAddTrackerTest extends SysuiTestCase {
+ private static final int END_POSITION = -1;
private static final int USER = 0;
@Mock
@@ -142,6 +143,29 @@
}
@Test
+ public void testRestoredTilePositionPreserved() {
+ verify(mBroadcastDispatcher).registerReceiver(
+ mBroadcastReceiverArgumentCaptor.capture(), any(), any(), any(), anyInt(), any());
+ String restoredTiles = "saver,internet,work,cast";
+ Intent restoreTilesIntent = makeRestoreIntent(Secure.QS_TILES, null, restoredTiles);
+
+ mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, restoreTilesIntent);
+
+ assertEquals(2, mAutoTracker.getRestoredTilePosition("work"));
+ }
+
+ @Test
+ public void testNoRestoredTileReturnsEndPosition() {
+ verify(mBroadcastDispatcher).registerReceiver(
+ mBroadcastReceiverArgumentCaptor.capture(), any(), any(), any(), anyInt(), any());
+ Intent restoreTilesIntent = makeRestoreIntent(Secure.QS_TILES, null, null);
+
+ mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, restoreTilesIntent);
+
+ assertEquals(END_POSITION, mAutoTracker.getRestoredTilePosition("work"));
+ }
+
+ @Test
public void testBroadcastReceiverRegistered() {
verify(mBroadcastDispatcher).registerReceiver(
any(), mIntentFilterArgumentCaptor.capture(), any(), eq(UserHandle.of(USER)),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 42ef9c2..4caa50f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -60,6 +60,7 @@
import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -494,7 +495,7 @@
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
- CommandQueue commandQueue = new CommandQueue(context);
+ CommandQueue commandQueue = new CommandQueue(context, new FakeDisplayTracker(context));
setupQsComponent();
setUpViews();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
index 2bd068a..8644b5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
@@ -35,12 +35,14 @@
import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.LaunchableFrameLayout
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.settings.FakeDisplayTracker
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
@@ -90,6 +92,7 @@
private lateinit var customTile: CustomTile
private lateinit var testableLooper: TestableLooper
private lateinit var customTileBuilder: CustomTile.Builder
+ private val displayTracker = FakeDisplayTracker(mContext)
@Before
fun setUp() {
@@ -119,7 +122,8 @@
activityStarter,
qsLogger,
customTileStatePersister,
- tileServices
+ tileServices,
+ displayTracker
)
customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
@@ -339,7 +343,7 @@
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
tile.qsTile.activityLaunchForClick = pi
- tile.handleClick(mock(View::class.java))
+ tile.handleClick(mock(LaunchableFrameLayout::class.java))
testableLooper.processAllMessages()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
index ca3182a..3281fa9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -28,7 +28,6 @@
import com.android.systemui.qs.tiles.BluetoothTile
import com.android.systemui.qs.tiles.CameraToggleTile
import com.android.systemui.qs.tiles.CastTile
-import com.android.systemui.qs.tiles.CellularTile
import com.android.systemui.qs.tiles.ColorCorrectionTile
import com.android.systemui.qs.tiles.ColorInversionTile
import com.android.systemui.qs.tiles.DataSaverTile
@@ -49,7 +48,6 @@
import com.android.systemui.qs.tiles.RotationLockTile
import com.android.systemui.qs.tiles.ScreenRecordTile
import com.android.systemui.qs.tiles.UiModeNightTile
-import com.android.systemui.qs.tiles.WifiTile
import com.android.systemui.qs.tiles.WorkModeTile
import com.android.systemui.util.leak.GarbageMonitor
import com.google.common.truth.Truth.assertThat
@@ -63,10 +61,8 @@
import org.mockito.Mockito.`when` as whenever
private val specMap = mapOf(
- "wifi" to WifiTile::class.java,
"internet" to InternetTile::class.java,
"bt" to BluetoothTile::class.java,
- "cell" to CellularTile::class.java,
"dnd" to DndTile::class.java,
"inversion" to ColorInversionTile::class.java,
"airplane" to AirplaneModeTile::class.java,
@@ -102,10 +98,8 @@
@Mock(answer = Answers.RETURNS_SELF) private lateinit var customTileBuilder: CustomTile.Builder
@Mock private lateinit var customTile: CustomTile
- @Mock private lateinit var wifiTile: WifiTile
@Mock private lateinit var internetTile: InternetTile
@Mock private lateinit var bluetoothTile: BluetoothTile
- @Mock private lateinit var cellularTile: CellularTile
@Mock private lateinit var dndTile: DndTile
@Mock private lateinit var colorInversionTile: ColorInversionTile
@Mock private lateinit var airplaneTile: AirplaneModeTile
@@ -146,10 +140,8 @@
factory = QSFactoryImpl(
{ qsHost },
{ customTileBuilder },
- { wifiTile },
{ internetTile },
{ bluetoothTile },
- { cellularTile },
{ dndTile },
{ colorInversionTile },
{ airplaneTile },
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
index 80c39cf..addca9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -37,10 +37,12 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
import com.android.systemui.statusbar.connectivity.AccessPointController;
import com.android.systemui.statusbar.connectivity.IconState;
import com.android.systemui.statusbar.connectivity.NetworkController;
+import com.android.systemui.statusbar.connectivity.WifiIndicators;
import org.junit.Before;
import org.junit.Test;
@@ -135,4 +137,24 @@
assertThat(mTile.getState().secondaryLabel)
.isNotEqualTo(mContext.getString(R.string.status_bar_airplane));
}
+
+ @Test
+ public void setIsAirplaneMode_APM_enabled_after_wifi_disconnected() {
+ WifiIndicators wifiIndicators = new WifiIndicators(
+ /* enabled= */ true,
+ /* statusIcon= */ null,
+ /* qsIcon= */ null,
+ /* activityIn= */ false,
+ /* activityOut= */ false,
+ /* description= */ null,
+ /* isTransient= */ false,
+ /* statusLabel= */ null
+ );
+ mTile.mSignalCallback.setWifiIndicators(wifiIndicators);
+ IconState state = new IconState(true, 0, "");
+ mTile.mSignalCallback.setIsAirplaneMode(state);
+ mTestableLooper.processAllMessages();
+ assertThat(mTile.getState().icon).isEqualTo(
+ QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_unavailable));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index 08a90b7..18e40f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,7 +30,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.qs.QSUserSwitcherEvent
-import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.statusbar.policy.UserSwitcherController
import com.android.systemui.user.data.source.UserRecord
import org.junit.Assert.assertEquals
@@ -42,7 +41,6 @@
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
-import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -152,15 +150,6 @@
assertNull(adapter.users.find { it.isManageUsers })
}
- @Test
- fun clickDismissDialog() {
- val shower: UserSwitchDialogController.DialogShower =
- mock(UserSwitchDialogController.DialogShower::class.java)
- adapter.injectDialogShower(shower)
- adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
- verify(shower).dismiss()
- }
-
private fun createUserRecord(current: Boolean, guest: Boolean) =
UserRecord(
UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
index b6a595b..7ba2cf7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
@@ -35,9 +35,33 @@
@Test
fun testCreateShareIntent() {
val uri = Uri.parse("content://fake")
+
+ val output = ActionIntentCreator.createShareIntent(uri)
+
+ assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
+ assertFlagsSet(
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK or
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ output.flags
+ )
+
+ val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+ assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND)
+ assertThat(wrappedIntent?.data).isEqualTo(uri)
+ assertThat(wrappedIntent?.type).isEqualTo("image/png")
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull()
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isNull()
+ assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
+ .isEqualTo(uri)
+ }
+
+ @Test
+ fun testCreateShareIntentWithSubject() {
+ val uri = Uri.parse("content://fake")
val subject = "Example subject"
- val output = ActionIntentCreator.createShareIntent(uri, subject)
+ val output = ActionIntentCreator.createShareIntentWithSubject(uri, subject)
assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
assertFlagsSet(
@@ -52,16 +76,34 @@
assertThat(wrappedIntent?.data).isEqualTo(uri)
assertThat(wrappedIntent?.type).isEqualTo("image/png")
assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo(subject)
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isNull()
assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
.isEqualTo(uri)
}
@Test
- fun testCreateShareIntent_noSubject() {
+ fun testCreateShareIntentWithExtraText() {
val uri = Uri.parse("content://fake")
- val output = ActionIntentCreator.createShareIntent(uri, null)
+ val extraText = "Extra text"
+
+ val output = ActionIntentCreator.createShareIntentWithExtraText(uri, extraText)
+
+ assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER)
+ assertFlagsSet(
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK or
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ output.flags
+ )
+
val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+ assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND)
+ assertThat(wrappedIntent?.data).isEqualTo(uri)
+ assertThat(wrappedIntent?.type).isEqualTo("image/png")
assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull()
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(extraText)
+ assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
+ .isEqualTo(uri)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java
index e1eda11..d5014fa36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java
@@ -39,6 +39,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -67,6 +68,7 @@
private PendingIntent mMockPendingIntent;
private Intent mIntent;
+ private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
@Before
public void setup() throws InterruptedException, ExecutionException, TimeoutException {
@@ -135,10 +137,11 @@
if (withStatusBar) {
return new ActionProxyReceiver(
Optional.of(mMockCentralSurfaces), mMockActivityManagerWrapper,
- mMockScreenshotSmartActions);
+ mMockScreenshotSmartActions, mDisplayTracker);
} else {
return new ActionProxyReceiver(
- Optional.empty(), mMockActivityManagerWrapper, mMockScreenshotSmartActions);
+ Optional.empty(), mMockActivityManagerWrapper, mMockScreenshotSmartActions,
+ mDisplayTracker);
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
new file mode 100644
index 0000000..9f0a803
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -0,0 +1,143 @@
+package com.android.systemui.screenshot
+
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.Guideline
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MessageContainerControllerTest : SysuiTestCase() {
+ lateinit var messageContainer: MessageContainerController
+
+ @Mock lateinit var workProfileMessageController: WorkProfileMessageController
+
+ @Mock lateinit var screenshotDetectionController: ScreenshotDetectionController
+
+ @Mock lateinit var icon: Drawable
+
+ lateinit var workProfileFirstRunView: ViewGroup
+ lateinit var detectionNoticeView: ViewGroup
+ lateinit var container: FrameLayout
+
+ var featureFlags = FakeFeatureFlags()
+ lateinit var screenshotView: ViewGroup
+
+ val userHandle = UserHandle.of(5)
+ val screenshotData = ScreenshotData.forTesting()
+
+ val appName = "app name"
+ lateinit var workProfileData: WorkProfileMessageController.WorkProfileFirstRunData
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ messageContainer =
+ MessageContainerController(
+ workProfileMessageController,
+ screenshotDetectionController,
+ featureFlags
+ )
+ screenshotView = ConstraintLayout(mContext)
+ workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
+
+ val guideline = Guideline(mContext)
+ guideline.id = com.android.systemui.R.id.guideline
+ screenshotView.addView(guideline)
+
+ container = FrameLayout(mContext)
+ container.id = com.android.systemui.R.id.screenshot_message_container
+ screenshotView.addView(container)
+
+ workProfileFirstRunView = FrameLayout(mContext)
+ workProfileFirstRunView.id = com.android.systemui.R.id.work_profile_first_run
+ container.addView(workProfileFirstRunView)
+
+ detectionNoticeView = FrameLayout(mContext)
+ detectionNoticeView.id = com.android.systemui.R.id.screenshot_detection_notice
+ container.addView(detectionNoticeView)
+
+ messageContainer.setView(screenshotView)
+
+ screenshotData.userHandle = userHandle
+ }
+
+ @Test
+ fun testOnScreenshotTakenUserHandle_noWorkProfileFirstRun() {
+ featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+ // (just being explicit here)
+ whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(null)
+
+ messageContainer.onScreenshotTaken(userHandle)
+
+ verify(workProfileMessageController, never()).populateView(any(), any(), any())
+ }
+
+ @Test
+ fun testOnScreenshotTakenUserHandle_noWorkProfileFlag() {
+ featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+ messageContainer.onScreenshotTaken(userHandle)
+
+ verify(workProfileMessageController, never()).onScreenshotTaken(any())
+ verify(workProfileMessageController, never()).populateView(any(), any(), any())
+ }
+
+ @Test
+ fun testOnScreenshotTakenUserHandle_withWorkProfileFirstRun() {
+ featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+ whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle)))
+ .thenReturn(workProfileData)
+ messageContainer.onScreenshotTaken(userHandle)
+
+ verify(workProfileMessageController)
+ .populateView(eq(workProfileFirstRunView), eq(workProfileData), any())
+ assertEquals(View.VISIBLE, workProfileFirstRunView.visibility)
+ assertEquals(View.GONE, detectionNoticeView.visibility)
+ }
+
+ @Test
+ fun testOnScreenshotTakenScreenshotData_flagsOff() {
+ featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+ featureFlags.set(Flags.SCREENSHOT_DETECTION, false)
+
+ messageContainer.onScreenshotTaken(screenshotData)
+
+ verify(workProfileMessageController, never()).onScreenshotTaken(any())
+ verify(screenshotDetectionController, never()).maybeNotifyOfScreenshot(any())
+
+ assertEquals(View.GONE, container.visibility)
+ }
+
+ @Test
+ fun testOnScreenshotTakenScreenshotData_nothingToShow() {
+ featureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+ featureFlags.set(Flags.SCREENSHOT_DETECTION, true)
+
+ messageContainer.onScreenshotTaken(screenshotData)
+
+ verify(workProfileMessageController, never()).populateView(any(), any(), any())
+ verify(screenshotDetectionController, never()).populateView(any(), any())
+
+ assertEquals(View.GONE, container.visibility)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 46a502a..541d6c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -22,15 +22,12 @@
import android.graphics.Insets
import android.graphics.Rect
import android.hardware.HardwareBuffer
-import android.os.Bundle
import android.os.UserHandle
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
-import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap
-import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
+import com.android.internal.util.ScreenshotRequest
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
@@ -49,7 +46,6 @@
private val bounds = Rect(25, 25, 75, 75)
private val scope = CoroutineScope(Dispatchers.Unconfined)
- private val dispatcher = Dispatchers.Unconfined
private val policy = FakeScreenshotPolicy()
private val flags = FakeFeatureFlags()
@@ -58,7 +54,8 @@
fun testProcessAsync() {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
- val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
var result: ScreenshotRequest? = null
@@ -76,17 +73,47 @@
assertThat(result).isEqualTo(request)
}
+ /** Tests the Java-compatible function wrapper, ensures callback is invoked. */
+ @Test
+ fun testProcessAsync_ScreenshotData() {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+ val request = ScreenshotData.fromRequest(
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build())
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ var result: ScreenshotData? = null
+ var callbackCount = 0
+ val callback: (ScreenshotData) -> Unit = { processedRequest: ScreenshotData ->
+ result = processedRequest
+ callbackCount++
+ }
+
+ // runs synchronously, using Unconfined Dispatcher
+ processor.processAsync(request, callback)
+
+ // Callback invoked once returning the same request (no changes)
+ assertThat(callbackCount).isEqualTo(1)
+ assertThat(result).isEqualTo(request)
+ }
+
@Test
fun testFullScreenshot_workProfilePolicyDisabled() = runBlocking {
flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
- val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
// No changes
assertThat(processedRequest).isEqualTo(request)
+
+ val screenshotData = ScreenshotData.fromRequest(request)
+ val processedData = processor.process(screenshotData)
+
+ assertThat(processedData).isEqualTo(screenshotData)
}
@Test
@@ -97,9 +124,11 @@
policy.setManagedProfile(USER_ID, false)
policy.setDisplayContentInfo(
policy.getDefaultDisplayId(),
- DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
+ DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+ )
- val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
@@ -108,6 +137,13 @@
assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER)
assertThat(processedRequest.topComponent).isEqualTo(component)
+
+ val processedData = processor.process(ScreenshotData.fromRequest(request))
+
+ // Request has topComponent added, but otherwise unchanged.
+ assertThat(processedData.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+ assertThat(processedData.source).isEqualTo(SCREENSHOT_OTHER)
+ assertThat(processedData.topComponent).isEqualTo(component)
}
@Test
@@ -120,23 +156,38 @@
// Indicate that the primary content belongs to a manged profile
policy.setManagedProfile(USER_ID, true)
- policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
- DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
+ policy.setDisplayContentInfo(
+ policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)
+ )
- val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD).build()
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val processedRequest = processor.process(request)
// Expect a task snapshot is taken, overriding the full screen mode
assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
- assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
+ assertThat(bitmap.equalsHardwareBitmap(processedRequest.bitmap)).isTrue()
assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
assertThat(processedRequest.userId).isEqualTo(USER_ID)
assertThat(processedRequest.topComponent).isEqualTo(component)
+
+ val processedData = processor.process(ScreenshotData.fromRequest(request))
+
+ // Expect a task snapshot is taken, overriding the full screen mode
+ assertThat(processedData.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
+ assertThat(processedData.bitmap).isEqualTo(bitmap)
+ assertThat(processedData.screenBounds).isEqualTo(bounds)
+ assertThat(processedData.insets).isEqualTo(Insets.NONE)
+ assertThat(processedData.taskId).isEqualTo(TASK_ID)
+ assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
+ assertThat(processedRequest.userId).isEqualTo(USER_ID)
+ assertThat(processedRequest.topComponent).isEqualTo(component)
}
@Test
@@ -147,15 +198,26 @@
val processor = RequestProcessor(imageCapture, policy, flags, scope)
val bitmap = makeHardwareBitmap(100, 100)
- val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
- val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
- bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+ .setTopComponent(component)
+ .setTaskId(TASK_ID)
+ .setUserId(USER_ID)
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(bounds)
+ .setInsets(Insets.NONE)
+ .build()
val processedRequest = processor.process(request)
// No changes
assertThat(processedRequest).isEqualTo(request)
+
+ val screenshotData = ScreenshotData.fromRequest(request)
+ val processedData = processor.process(screenshotData)
+
+ assertThat(processedData).isEqualTo(screenshotData)
}
@Test
@@ -168,15 +230,26 @@
policy.setManagedProfile(USER_ID, false)
val bitmap = makeHardwareBitmap(100, 100)
- val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
- val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
- bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+ .setTopComponent(component)
+ .setTaskId(TASK_ID)
+ .setUserId(USER_ID)
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(bounds)
+ .setInsets(Insets.NONE)
+ .build()
val processedRequest = processor.process(request)
// No changes
assertThat(processedRequest).isEqualTo(request)
+
+ val screenshotData = ScreenshotData.fromRequest(request)
+ val processedData = processor.process(screenshotData)
+
+ assertThat(processedData).isEqualTo(screenshotData)
}
@Test
@@ -190,26 +263,41 @@
policy.setManagedProfile(USER_ID, true)
val bitmap = makeHardwareBitmap(100, 100)
- val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
- val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
- bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+ .setTopComponent(component)
+ .setTaskId(TASK_ID)
+ .setUserId(USER_ID)
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(bounds)
+ .setInsets(Insets.NONE)
+ .build()
val processedRequest = processor.process(request)
// Work profile, but already a task snapshot, so no changes
assertThat(processedRequest).isEqualTo(request)
+
+ val screenshotData = ScreenshotData.fromRequest(request)
+ val processedData = processor.process(screenshotData)
+
+ assertThat(processedData).isEqualTo(screenshotData)
}
private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
- val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
- HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
+ val buffer =
+ HardwareBuffer.create(
+ width,
+ height,
+ HardwareBuffer.RGBA_8888,
+ 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ )
return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
}
- private fun Bitmap.equalsHardwareBitmapBundle(bundle: Bundle): Boolean {
- val provided = bundleToHardwareBitmap(bundle)
- return provided.hardwareBuffer == this.hardwareBuffer &&
- provided.colorSpace == this.colorSpace
+ private fun Bitmap.equalsHardwareBitmap(bitmap: Bitmap): Boolean {
+ return bitmap.hardwareBuffer == this.hardwareBuffer && bitmap.colorSpace == this.colorSpace
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
new file mode 100644
index 0000000..43e9939
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.screenshot
+
+import android.content.ComponentName
+import android.graphics.Insets
+import android.graphics.Rect
+import android.os.UserHandle
+import android.view.WindowManager
+import com.android.internal.util.ScreenshotRequest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ScreenshotDataTest {
+ private val type = WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+ private val source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
+ private val bounds = Rect(1, 2, 3, 4)
+ private val taskId = 123
+ private val userId = 1
+ private val insets = Insets.of(1, 2, 3, 4)
+ private val component = ComponentName("android.test", "android.test.Component")
+
+ @Test
+ fun testConstruction() {
+ val request =
+ ScreenshotRequest.Builder(type, source)
+ .setBoundsOnScreen(bounds)
+ .setInsets(insets)
+ .setTaskId(taskId)
+ .setUserId(userId)
+ .setTopComponent(component)
+ .build()
+
+ val data = ScreenshotData.fromRequest(request)
+
+ assertThat(data.source).isEqualTo(source)
+ assertThat(data.type).isEqualTo(type)
+ assertThat(data.screenBounds).isEqualTo(bounds)
+ assertThat(data.insets).isEqualTo(insets)
+ assertThat(data.taskId).isEqualTo(taskId)
+ assertThat(data.userHandle).isEqualTo(UserHandle.of(userId))
+ assertThat(data.topComponent).isEqualTo(component)
+ }
+
+ @Test
+ fun testNegativeUserId() {
+ val request = ScreenshotRequest.Builder(type, source).setUserId(-1).build()
+
+ val data = ScreenshotData.fromRequest(request)
+
+ assertThat(data.userHandle).isNull()
+ }
+
+ @Test
+ fun testPackageNameAsString() {
+ val request = ScreenshotRequest.Builder(type, source).setTopComponent(component).build()
+
+ val data = ScreenshotData.fromRequest(request)
+
+ assertThat(data.packageNameString).isEqualTo("android.test")
+ }
+
+ @Test
+ fun testPackageNameAsString_null() {
+ val request = ScreenshotRequest.Builder(type, source).build()
+
+ val data = ScreenshotData.fromRequest(request)
+
+ assertThat(data.packageNameString).isEqualTo("")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
index 17396b1..e70fa2f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
@@ -31,6 +31,7 @@
import android.testing.AndroidTestingRunner
import com.android.systemui.SysuiTestCase
import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+import com.android.systemui.settings.FakeDisplayTracker
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -126,8 +127,10 @@
val userManager = mock<UserManager>()
val atmService = mock<IActivityTaskManager>()
val dispatcher = Dispatchers.Unconfined
+ val displayTracker = FakeDisplayTracker(mContext)
- return object : ScreenshotPolicyImpl(context, userManager, atmService, dispatcher) {
+ return object : ScreenshotPolicyImpl(context, userManager, atmService, dispatcher,
+ displayTracker) {
override suspend fun isManagedProfile(userId: Int) = (userId == MANAGED_PROFILE_USER)
override suspend fun getAllRootTaskInfosOnDisplay(displayId: Int) = tasks
override suspend fun isNotificationShadeExpanded() = shadeExpanded
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index fa1fedb..74969d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -29,19 +29,18 @@
import android.os.UserHandle
import android.os.UserManager
import android.testing.AndroidTestingRunner
-import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.util.ScreenshotHelper
-import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
+import com.android.internal.util.ScreenshotRequest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR
import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
-import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD
+import com.android.systemui.flags.Flags.SCREENSHOT_METADATA
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
import com.android.systemui.util.mockito.any
@@ -81,15 +80,28 @@
private val flags = FakeFeatureFlags()
private val topComponent = ComponentName(mContext, TakeScreenshotServiceTest::class.java)
- private val service = TakeScreenshotService(
- controller, userManager, devicePolicyManager, eventLogger,
- notificationsController, mContext, Runnable::run, flags, requestProcessor)
+ private val service =
+ TakeScreenshotService(
+ controller,
+ userManager,
+ devicePolicyManager,
+ eventLogger,
+ notificationsController,
+ mContext,
+ Runnable::run,
+ flags,
+ requestProcessor
+ )
@Before
fun setUp() {
whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
- whenever(devicePolicyManager.getScreenCaptureDisabled(
- /* admin component (null: any admin) */ isNull(), eq(UserHandle.USER_ALL)))
+ whenever(
+ devicePolicyManager.getScreenCaptureDisabled(
+ /* admin component (null: any admin) */ isNull(),
+ eq(UserHandle.USER_ALL)
+ )
+ )
.thenReturn(false)
whenever(userManager.isUserUnlocked).thenReturn(true)
@@ -98,11 +110,19 @@
val request: ScreenshotRequest = it.getArgument(0) as ScreenshotRequest
val consumer: Consumer<ScreenshotRequest> = it.getArgument(1)
consumer.accept(request)
- }.`when`(requestProcessor).processAsync(/* request= */ any(), /* callback= */ any())
+ }.`when`(requestProcessor).processAsync(
+ /* request= */ any(ScreenshotRequest::class.java), /* callback= */ any())
+
+ doAnswer {
+ val request: ScreenshotData = it.getArgument(0) as ScreenshotData
+ val consumer: Consumer<ScreenshotData> = it.getArgument(1)
+ consumer.accept(request)
+ }.`when`(requestProcessor).processAsync(
+ /* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
// Flipped in selected test cases
- flags.set(SCREENSHOT_REQUEST_PROCESSOR, false)
flags.set(SCREENSHOT_WORK_PROFILE_POLICY, false)
+ flags.set(SCREENSHOT_METADATA, false)
service.attach(
mContext,
@@ -110,7 +130,8 @@
/* className = */ null,
/* token = */ null,
application,
- /* activityManager = */ null)
+ /* activityManager = */ null
+ )
}
@Test
@@ -127,40 +148,41 @@
@Test
fun takeScreenshotFullscreen() {
- val request = ScreenshotRequest(
- TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_KEY_CHORD,
- topComponent)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
- service.handleRequest(request, { /* onSaved */ }, callback)
+ service.handleRequest(request, { /* onSaved */}, callback)
- verify(controller, times(1)).takeScreenshotFullscreen(
- eq(topComponent),
- /* onSavedListener = */ any(),
- /* requestCallback = */ any())
+ verify(controller, times(1))
+ .takeScreenshotFullscreen(
+ eq(topComponent),
+ /* onSavedListener = */ any(),
+ /* requestCallback = */ any()
+ )
assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
val logEvent = eventLogger.get(0)
assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id)
+ logEvent.eventId, SCREENSHOT_REQUESTED_KEY_OTHER.id)
assertEquals("Expected supplied package name",
topComponent.packageName, eventLogger.get(0).packageName)
}
@Test
- fun takeScreenshot_requestProcessorEnabled() {
- flags.set(SCREENSHOT_REQUEST_PROCESSOR, true)
+ fun takeScreenshotFullscreen_screenshotDataEnabled() {
+ flags.set(SCREENSHOT_METADATA, true)
- val request = ScreenshotRequest(
+ val request = ScreenshotRequest.Builder(
TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_KEY_CHORD,
- topComponent)
+ SCREENSHOT_KEY_OTHER).setTopComponent(topComponent).build()
service.handleRequest(request, { /* onSaved */ }, callback)
- verify(controller, times(1)).takeScreenshotFullscreen(
- eq(topComponent),
+ verify(controller, times(1)).handleScreenshot(
+ eq(ScreenshotData.fromRequest(request)),
/* onSavedListener = */ any(),
/* requestCallback = */ any())
@@ -168,7 +190,7 @@
val logEvent = eventLogger.get(0)
assertEquals("Expected SCREENSHOT_REQUESTED UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id)
+ logEvent.eventId, SCREENSHOT_REQUESTED_KEY_OTHER.id)
assertEquals("Expected supplied package name",
topComponent.packageName, eventLogger.get(0).packageName)
}
@@ -177,38 +199,56 @@
fun takeScreenshotProvidedImage() {
val bounds = Rect(50, 50, 150, 150)
val bitmap = makeHardwareBitmap(100, 100)
- val bitmapBundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
- val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW,
- bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, topComponent)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW)
+ .setTopComponent(topComponent)
+ .setTaskId(TASK_ID)
+ .setUserId(USER_ID)
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(bounds)
+ .setInsets(Insets.NONE)
+ .build()
- service.handleRequest(request, { /* onSaved */ }, callback)
+ service.handleRequest(request, { /* onSaved */}, callback)
- verify(controller, times(1)).handleImageAsScreenshot(
- argThat { b -> b.equalsHardwareBitmap(bitmap) },
- eq(bounds),
- eq(Insets.NONE), eq(TASK_ID), eq(USER_ID), eq(topComponent),
- /* onSavedListener = */ any(), /* requestCallback = */ any())
+ verify(controller, times(1))
+ .handleImageAsScreenshot(
+ argThat { b -> b.equalsHardwareBitmap(bitmap) },
+ eq(bounds),
+ eq(Insets.NONE),
+ eq(TASK_ID),
+ eq(USER_ID),
+ eq(topComponent),
+ /* onSavedListener = */ any(),
+ /* requestCallback = */ any()
+ )
assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
val logEvent = eventLogger.get(0)
- assertEquals("Expected SCREENSHOT_REQUESTED_* UiEvent",
- logEvent.eventId, SCREENSHOT_REQUESTED_OVERVIEW.id)
- assertEquals("Expected supplied package name",
- topComponent.packageName, eventLogger.get(0).packageName)
+ assertEquals(
+ "Expected SCREENSHOT_REQUESTED_* UiEvent",
+ logEvent.eventId,
+ SCREENSHOT_REQUESTED_OVERVIEW.id
+ )
+ assertEquals(
+ "Expected supplied package name",
+ topComponent.packageName,
+ eventLogger.get(0).packageName
+ )
}
@Test
fun takeScreenshotFullscreen_userLocked() {
whenever(userManager.isUserUnlocked).thenReturn(false)
- val request = ScreenshotRequest(
- TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_KEY_CHORD,
- topComponent)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
- service.handleRequest(request, { /* onSaved */ }, callback)
+ service.handleRequest(request, { /* onSaved */}, callback)
verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
verify(callback, times(1)).reportError()
@@ -217,21 +257,24 @@
@Test
fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
- whenever(devicePolicyManager.getScreenCaptureDisabled(
- isNull(), eq(UserHandle.USER_ALL))
- ).thenReturn(true)
+ whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
+ .thenReturn(true)
- whenever(devicePolicyResourcesManager.getString(
- eq(SCREENSHOT_BLOCKED_BY_ADMIN),
- /* Supplier<String> */ any(),
- )).thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+ whenever(
+ devicePolicyResourcesManager.getString(
+ eq(SCREENSHOT_BLOCKED_BY_ADMIN),
+ /* Supplier<String> */
+ any(),
+ )
+ )
+ .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
- val request = ScreenshotRequest(
- TAKE_SCREENSHOT_FULLSCREEN,
- SCREENSHOT_KEY_CHORD,
- topComponent)
+ val request =
+ ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
+ .setTopComponent(topComponent)
+ .build()
- service.handleRequest(request, { /* onSaved */ }, callback)
+ service.handleRequest(request, { /* onSaved */}, callback)
// error shown: Toast.makeText(...).show(), untestable
verify(callback, times(1)).reportError()
@@ -241,14 +284,20 @@
private fun Bitmap.equalsHardwareBitmap(other: Bitmap): Boolean {
return config == HARDWARE &&
- other.config == HARDWARE &&
- hardwareBuffer == other.hardwareBuffer &&
- colorSpace == other.colorSpace
+ other.config == HARDWARE &&
+ hardwareBuffer == other.hardwareBuffer &&
+ colorSpace == other.colorSpace
}
/** A hardware Bitmap is mandated by use of ScreenshotHelper.HardwareBitmapBundler */
private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
- val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
- HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
+ val buffer =
+ HardwareBuffer.create(
+ width,
+ height,
+ HardwareBuffer.RGBA_8888,
+ 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
+ )
return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
new file mode 100644
index 0000000..3440f91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/WorkProfileMessageControllerTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.testing.AndroidTestingRunner;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.FakeSharedPreferences;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import kotlin.Unit;
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class WorkProfileMessageControllerTest extends SysuiTestCase {
+ private static final String DEFAULT_LABEL = "default label";
+ private static final String APP_LABEL = "app label";
+ private static final UserHandle NON_WORK_USER = UserHandle.of(0);
+ private static final UserHandle WORK_USER = UserHandle.of(10);
+
+ @Mock
+ private UserManager mUserManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private Context mMockContext;
+ @Mock
+ private Drawable mActivityIcon;
+ @Mock
+ private Drawable mBadgedActivityIcon;
+ @Mock
+ private ActivityInfo mActivityInfo;
+
+ private FakeSharedPreferences mSharedPreferences = new FakeSharedPreferences();
+
+ private WorkProfileMessageController mMessageController;
+
+ @Before
+ public void setup() throws PackageManager.NameNotFoundException {
+ MockitoAnnotations.initMocks(this);
+
+ when(mUserManager.isManagedProfile(eq(WORK_USER.getIdentifier()))).thenReturn(true);
+ when(mMockContext.getSharedPreferences(
+ eq(WorkProfileMessageController.SHARED_PREFERENCES_NAME),
+ eq(Context.MODE_PRIVATE))).thenReturn(mSharedPreferences);
+ when(mMockContext.getString(ArgumentMatchers.anyInt())).thenReturn(DEFAULT_LABEL);
+ when(mPackageManager.getActivityIcon(any(ComponentName.class)))
+ .thenReturn(mActivityIcon);
+ when(mPackageManager.getUserBadgedIcon(
+ any(), any())).thenReturn(mBadgedActivityIcon);
+ when(mPackageManager.getActivityInfo(any(),
+ any(PackageManager.ComponentInfoFlags.class))).thenReturn(mActivityInfo);
+ when(mActivityInfo.loadLabel(eq(mPackageManager))).thenReturn(APP_LABEL);
+
+ mSharedPreferences.edit().putBoolean(
+ WorkProfileMessageController.PREFERENCE_KEY, false).apply();
+
+ mMessageController = new WorkProfileMessageController(mMockContext, mUserManager,
+ mPackageManager);
+ }
+
+ @Test
+ public void testOnScreenshotTaken_notManaged() {
+ assertNull(mMessageController.onScreenshotTaken(NON_WORK_USER));
+ }
+
+ @Test
+ public void testOnScreenshotTaken_alreadyDismissed() {
+ mSharedPreferences.edit().putBoolean(
+ WorkProfileMessageController.PREFERENCE_KEY, true).apply();
+
+ assertNull(mMessageController.onScreenshotTaken(WORK_USER));
+ }
+
+ @Test
+ public void testOnScreenshotTaken_packageNotFound()
+ throws PackageManager.NameNotFoundException {
+ when(mPackageManager.getActivityInfo(any(),
+ any(PackageManager.ComponentInfoFlags.class))).thenThrow(
+ new PackageManager.NameNotFoundException());
+
+ WorkProfileMessageController.WorkProfileFirstRunData data =
+ mMessageController.onScreenshotTaken(WORK_USER);
+
+ assertEquals(DEFAULT_LABEL, data.getAppName());
+ assertNull(data.getIcon());
+ }
+
+ @Test
+ public void testOnScreenshotTaken() {
+ WorkProfileMessageController.WorkProfileFirstRunData data =
+ mMessageController.onScreenshotTaken(WORK_USER);
+
+ assertEquals(APP_LABEL, data.getAppName());
+ assertEquals(mBadgedActivityIcon, data.getIcon());
+ }
+
+ @Test
+ public void testPopulateView() throws InterruptedException {
+ ViewGroup layout = (ViewGroup) LayoutInflater.from(mContext).inflate(
+ R.layout.screenshot_work_profile_first_run, null);
+ WorkProfileMessageController.WorkProfileFirstRunData data =
+ new WorkProfileMessageController.WorkProfileFirstRunData(APP_LABEL,
+ mBadgedActivityIcon);
+ final CountDownLatch countdown = new CountDownLatch(1);
+ mMessageController.populateView(layout, data, () -> {
+ countdown.countDown();
+ return Unit.INSTANCE;
+ });
+
+ ImageView image = layout.findViewById(R.id.screenshot_message_icon);
+ assertEquals(mBadgedActivityIcon, image.getDrawable());
+ TextView text = layout.findViewById(R.id.screenshot_message_content);
+ // The app name is used in a template, but at least validate that it was inserted.
+ assertTrue(text.getText().toString().contains(APP_LABEL));
+
+ // Validate that clicking the dismiss button calls back properly.
+ assertEquals(1, countdown.getCount());
+ layout.findViewById(R.id.message_dismiss_button).callOnClick();
+ countdown.await(1000, TimeUnit.MILLISECONDS);
+ }
+}
+
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/DisplayTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/DisplayTrackerImplTest.kt
new file mode 100644
index 0000000..ae976a0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/DisplayTrackerImplTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings
+
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS
+import android.hardware.display.DisplayManagerGlobal
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DisplayTrackerImplTest : SysuiTestCase() {
+ @Mock private lateinit var displayManager: DisplayManager
+ @Mock private lateinit var handler: Handler
+
+ private val executor = Executor(Runnable::run)
+ private lateinit var mDefaultDisplay: Display
+ private lateinit var mSecondaryDisplay: Display
+ private lateinit var tracker: DisplayTrackerImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ mDefaultDisplay =
+ Display(
+ DisplayManagerGlobal.getInstance(),
+ Display.DEFAULT_DISPLAY,
+ DisplayInfo(),
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+ )
+ mSecondaryDisplay =
+ Display(
+ DisplayManagerGlobal.getInstance(),
+ Display.DEFAULT_DISPLAY + 1,
+ DisplayInfo(),
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+ )
+
+ `when`(displayManager.displays).thenReturn(arrayOf(mDefaultDisplay, mSecondaryDisplay))
+
+ tracker = DisplayTrackerImpl(displayManager, handler)
+ }
+
+ @Test
+ fun testGetDefaultDisplay() {
+ assertThat(tracker.defaultDisplayId).isEqualTo(Display.DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testGetAllDisplays() {
+ assertThat(tracker.allDisplays).isEqualTo(arrayOf(mDefaultDisplay, mSecondaryDisplay))
+ }
+
+ @Test
+ fun registerCallback_registersDisplayListener() {
+ tracker.addDisplayChangeCallback(TestCallback(), executor)
+ verify(displayManager).registerDisplayListener(any(), any())
+ }
+
+ @Test
+ fun registerBrightnessCallback_registersDisplayListener() {
+ tracker.addBrightnessChangeCallback(TestCallback(), executor)
+ verify(displayManager)
+ .registerDisplayListener(any(), any(), eq(EVENT_FLAG_DISPLAY_BRIGHTNESS))
+ }
+
+ @Test
+ fun unregisterCallback_displayListenerStillRegistered() {
+ val callback1 = TestCallback()
+ tracker.addDisplayChangeCallback(callback1, executor)
+ tracker.addDisplayChangeCallback(TestCallback(), executor)
+ tracker.removeCallback(callback1)
+
+ verify(displayManager, never()).unregisterDisplayListener(any())
+ }
+
+ @Test
+ fun unregisterLastCallback_unregistersDisplayListener() {
+ val callback = TestCallback()
+ tracker.addDisplayChangeCallback(callback, executor)
+ tracker.removeCallback(callback)
+
+ verify(displayManager).unregisterDisplayListener(any())
+ }
+
+ @Test
+ fun callbackCalledOnDisplayAdd() {
+ val testDisplay = 2
+ val callback = TestCallback()
+ tracker.addDisplayChangeCallback(callback, executor)
+ tracker.displayChangedListener.onDisplayAdded(testDisplay)
+
+ assertThat(callback.lastDisplayAdded).isEqualTo(testDisplay)
+ }
+
+ @Test
+ fun callbackCalledOnDisplayRemoved() {
+ val testDisplay = 2
+ val callback = TestCallback()
+ tracker.addDisplayChangeCallback(callback, executor)
+ tracker.displayChangedListener.onDisplayRemoved(testDisplay)
+
+ assertThat(callback.lastDisplayRemoved).isEqualTo(testDisplay)
+ }
+
+ @Test
+ fun callbackCalledOnDisplayChanged() {
+ val testDisplay = 2
+ val callback = TestCallback()
+ tracker.addDisplayChangeCallback(callback, executor)
+ tracker.displayChangedListener.onDisplayChanged(testDisplay)
+
+ assertThat(callback.lastDisplayChanged).isEqualTo(testDisplay)
+ }
+
+ @Test
+ fun callbackCalledOnBrightnessChanged() {
+ val testDisplay = 2
+ val callback = TestCallback()
+ tracker.addBrightnessChangeCallback(callback, executor)
+ tracker.displayBrightnessChangedListener.onDisplayChanged(testDisplay)
+
+ assertThat(callback.lastDisplayChanged).isEqualTo(testDisplay)
+ }
+
+ private class TestCallback : DisplayTracker.Callback {
+ var lastDisplayAdded = -1
+ var lastDisplayRemoved = -1
+ var lastDisplayChanged = -1
+
+ override fun onDisplayAdded(displayId: Int) {
+ lastDisplayAdded = displayId
+ }
+
+ override fun onDisplayRemoved(displayId: Int) {
+ lastDisplayRemoved = displayId
+ }
+
+ override fun onDisplayChanged(displayId: Int) {
+ lastDisplayChanged = displayId
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
new file mode 100644
index 0000000..57b6b2b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
@@ -0,0 +1,102 @@
+package com.android.systemui.settings
+
+import android.app.IActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.os.Handler
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.concurrent.futures.DirectExecutor
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(Parameterized::class)
+class UserTrackerImplReceiveTest : SysuiTestCase() {
+
+ companion object {
+
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Iterable<String> =
+ listOf(
+ Intent.ACTION_USER_INFO_CHANGED,
+ Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
+ Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
+ Intent.ACTION_MANAGED_PROFILE_ADDED,
+ Intent.ACTION_MANAGED_PROFILE_REMOVED,
+ Intent.ACTION_MANAGED_PROFILE_UNLOCKED
+ )
+ }
+
+ private val executor: Executor = DirectExecutor.INSTANCE
+
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var userManager: UserManager
+ @Mock private lateinit var iActivityManager: IActivityManager
+ @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager
+ @Mock(stubOnly = true) private lateinit var handler: Handler
+
+ @Parameterized.Parameter lateinit var intentAction: String
+ @Mock private lateinit var callback: UserTracker.Callback
+ @Captor private lateinit var captor: ArgumentCaptor<List<UserInfo>>
+
+ private lateinit var tracker: UserTrackerImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ `when`(context.user).thenReturn(UserHandle.SYSTEM)
+ `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context)
+
+ tracker = UserTrackerImpl(context, userManager, iActivityManager, dumpManager, handler)
+ }
+
+ @Test
+ fun `calls callback and updates profiles when an intent received`() {
+ tracker.initialize(0)
+ tracker.addCallback(callback, executor)
+ val profileID = tracker.userId + 10
+
+ `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+ val id = invocation.getArgument<Int>(0)
+ val info = UserInfo(id, "", UserInfo.FLAG_FULL)
+ val infoProfile =
+ UserInfo(
+ id + 10,
+ "",
+ "",
+ UserInfo.FLAG_MANAGED_PROFILE,
+ UserManager.USER_TYPE_PROFILE_MANAGED
+ )
+ infoProfile.profileGroupId = id
+ listOf(info, infoProfile)
+ }
+
+ tracker.onReceive(context, Intent(intentAction))
+
+ verify(callback, times(0)).onUserChanged(anyInt(), any())
+ verify(callback, times(1)).onProfilesChanged(capture(captor))
+ assertThat(captor.value.map { it.id }).containsExactly(0, profileID)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index 52462c7..71ba215 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -16,11 +16,14 @@
package com.android.systemui.settings
+import android.app.IActivityManager
+import android.app.IUserSwitchObserver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.UserInfo
import android.os.Handler
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.testing.AndroidTestingRunner
@@ -29,19 +32,20 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
-import org.mockito.Mockito.`when`
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -51,6 +55,10 @@
private lateinit var context: Context
@Mock
private lateinit var userManager: UserManager
+ @Mock
+ private lateinit var iActivityManager: IActivityManager
+ @Mock
+ private lateinit var userSwitchingReply: IRemoteCallback
@Mock(stubOnly = true)
private lateinit var dumpManager: DumpManager
@Mock(stubOnly = true)
@@ -76,7 +84,7 @@
listOf(info)
}
- tracker = UserTrackerImpl(context, userManager, dumpManager, handler)
+ tracker = UserTrackerImpl(context, userManager, iActivityManager, dumpManager, handler)
}
@Test
@@ -124,6 +132,15 @@
verify(context).registerReceiverForAllUsers(
eq(tracker), capture(captor), isNull(), eq(handler))
+ with(captor.value) {
+ assertThat(countActions()).isEqualTo(6)
+ assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue()
+ assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue()
+ }
}
@Test
@@ -148,8 +165,10 @@
tracker.initialize(0)
val newID = 5
- val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, newID)
- tracker.onReceive(context, intent)
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+ verify(userSwitchingReply).sendResult(any())
verify(userManager).getProfiles(newID)
@@ -262,6 +281,24 @@
}
@Test
+ fun testCallbackCalledOnUserChanging() {
+ tracker.initialize(0)
+ val callback = TestCallback()
+ tracker.addCallback(callback, executor)
+
+ val newID = 5
+
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+ verify(userSwitchingReply).sendResult(any())
+
+ assertThat(callback.calledOnUserChanging).isEqualTo(1)
+ assertThat(callback.lastUser).isEqualTo(newID)
+ assertThat(callback.lastUserContext?.userId).isEqualTo(newID)
+ }
+
+ @Test
fun testCallbackCalledOnUserChanged() {
tracker.initialize(0)
val callback = TestCallback()
@@ -269,8 +306,9 @@
val newID = 5
- val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, newID)
- tracker.onReceive(context, intent)
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onUserSwitchComplete(newID)
assertThat(callback.calledOnUserChanged).isEqualTo(1)
assertThat(callback.lastUser).isEqualTo(newID)
@@ -280,37 +318,6 @@
}
@Test
- fun testCallbackCalledOnProfileChanged() {
- tracker.initialize(0)
- val callback = TestCallback()
- tracker.addCallback(callback, executor)
- val profileID = tracker.userId + 10
-
- `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
- val id = invocation.getArgument<Int>(0)
- val info = UserInfo(id, "", UserInfo.FLAG_FULL)
- val infoProfile = UserInfo(
- id + 10,
- "",
- "",
- UserInfo.FLAG_MANAGED_PROFILE,
- UserManager.USER_TYPE_PROFILE_MANAGED
- )
- infoProfile.profileGroupId = id
- listOf(info, infoProfile)
- }
-
- val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
- .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
-
- tracker.onReceive(context, intent)
-
- assertThat(callback.calledOnUserChanged).isEqualTo(0)
- assertThat(callback.calledOnProfilesChanged).isEqualTo(1)
- assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID)
- }
-
- @Test
fun testCallbackCalledOnUserInfoChanged() {
tracker.initialize(0)
val callback = TestCallback()
@@ -351,25 +358,36 @@
tracker.addCallback(callback, executor)
tracker.removeCallback(callback)
- val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, 5)
- tracker.onReceive(context, intent)
+ val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
+ verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onUserSwitching(newID, userSwitchingReply)
+ verify(userSwitchingReply).sendResult(any())
+ captor.value.onUserSwitchComplete(newID)
val intentProfiles = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
.putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
tracker.onReceive(context, intentProfiles)
+ assertThat(callback.calledOnUserChanging).isEqualTo(0)
assertThat(callback.calledOnUserChanged).isEqualTo(0)
assertThat(callback.calledOnProfilesChanged).isEqualTo(0)
}
private class TestCallback : UserTracker.Callback {
+ var calledOnUserChanging = 0
var calledOnUserChanged = 0
var calledOnProfilesChanged = 0
var lastUser: Int? = null
var lastUserContext: Context? = null
var lastUserProfiles = emptyList<UserInfo>()
+ override fun onUserChanging(newUser: Int, userContext: Context) {
+ calledOnUserChanging++
+ lastUser = newUser
+ lastUserContext = userContext
+ }
+
override fun onUserChanged(newUser: Int, userContext: Context) {
calledOnUserChanged++
lastUser = newUser
@@ -381,4 +399,4 @@
lastUserProfiles = profiles
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index 9d1802a..58ade49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -28,6 +28,7 @@
import androidx.test.runner.intercepting.SingleActivityFactory
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
@@ -51,6 +52,7 @@
@Mock private lateinit var mainExecutor: Executor
@Mock private lateinit var backgroundHandler: Handler
@Mock private lateinit var brightnessSliderController: BrightnessSliderController
+ private val displayTracker = FakeDisplayTracker(mContext)
@Rule
@JvmField
@@ -60,6 +62,7 @@
override fun create(intent: Intent?): TestDialog {
return TestDialog(
userTracker,
+ displayTracker,
brightnessSliderControllerFactory,
mainExecutor,
backgroundHandler
@@ -105,12 +108,14 @@
class TestDialog(
userTracker: UserTracker,
+ displayTracker: FakeDisplayTracker,
brightnessSliderControllerFactory: BrightnessSliderController.Factory,
mainExecutor: Executor,
backgroundHandler: Handler
) :
BrightnessDialog(
userTracker,
+ displayTracker,
brightnessSliderControllerFactory,
mainExecutor,
backgroundHandler
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 88651c1..3706859 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import android.testing.AndroidTestingRunner
+import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
@@ -89,15 +90,15 @@
fun testEdgeElementsAlignedWithEdgeOrGuide_qs() {
with(qsConstraint) {
assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
- assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
+ assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0.5f)
assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID)
- assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0f)
+ assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0.5f)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd)
.isEqualTo(PARENT_ID)
assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias)
- .isEqualTo(1f)
+ .isEqualTo(0.5f)
assertThat(getConstraint(R.id.privacy_container).layout.endToEnd)
.isEqualTo(R.id.end_guide)
@@ -108,11 +109,12 @@
@Test
fun testEdgeElementsAlignedWithEdge_largeScreen() {
with(largeScreenConstraint) {
- assertThat(getConstraint(R.id.clock).layout.startToStart).isEqualTo(PARENT_ID)
- assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f)
+ assertThat(getConstraint(R.id.clock).layout.startToEnd).isEqualTo(R.id.begin_guide)
+ assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0.5f)
- assertThat(getConstraint(R.id.privacy_container).layout.endToEnd).isEqualTo(PARENT_ID)
- assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(1f)
+ assertThat(getConstraint(R.id.privacy_container).layout.endToStart)
+ .isEqualTo(R.id.end_guide)
+ assertThat(getConstraint(R.id.privacy_container).layout.horizontalBias).isEqualTo(0.5f)
}
}
@@ -218,7 +220,12 @@
.isEqualTo(cutoutEnd - padding)
}
- assertThat(changes.largeScreenConstraintsChanges).isNull()
+ with(largeScreenConstraint) {
+ assertThat(getConstraint(R.id.begin_guide).layout.guideBegin)
+ .isEqualTo(cutoutStart - padding)
+ assertThat(getConstraint(R.id.end_guide).layout.guideEnd)
+ .isEqualTo(cutoutEnd - padding)
+ }
}
@Test
@@ -245,7 +252,10 @@
assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
}
- assertThat(changes.largeScreenConstraintsChanges).isNull()
+ with(largeScreenConstraint) {
+ assertThat(getConstraint(R.id.begin_guide).layout.guideBegin).isEqualTo(0)
+ assertThat(getConstraint(R.id.end_guide).layout.guideEnd).isEqualTo(0)
+ }
}
@Test
@@ -331,10 +341,7 @@
val views = mapOf(
R.id.clock to "clock",
R.id.date to "date",
- R.id.statusIcons to "icons",
R.id.privacy_container to "privacy",
- R.id.carrier_group to "carriers",
- R.id.batteryRemainingIcon to "battery",
)
views.forEach { (id, name) ->
assertWithMessage("$name has 0 height in qqs")
@@ -352,11 +359,7 @@
fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
val views = mapOf(
R.id.clock to "clock",
- R.id.date to "date",
- R.id.statusIcons to "icons",
R.id.privacy_container to "privacy",
- R.id.carrier_group to "carriers",
- R.id.batteryRemainingIcon to "battery",
)
views.forEach { (id, name) ->
expect.withMessage("$name changes height")
@@ -369,8 +372,8 @@
}
private fun Int.fromConstraint() = when (this) {
- -1 -> "MATCH_PARENT"
- -2 -> "WRAP_CONTENT"
+ ViewGroup.LayoutParams.MATCH_PARENT -> "MATCH_PARENT"
+ ViewGroup.LayoutParams.WRAP_CONTENT -> "WRAP_CONTENT"
else -> toString()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index 1d30ad9..91fef1d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -182,6 +182,7 @@
null
}
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
+ whenever(view.alpha).thenReturn(1f)
whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
@@ -691,6 +692,19 @@
assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header)
}
+ @Test
+ fun `carrier left padding is set when clock layout changes`() {
+ val width = 200
+ whenever(clock.width).thenReturn(width)
+ whenever(clock.scaleX).thenReturn(2.57f) // 2.57 comes from qs_header.xml
+ val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+
+ verify(clock).addOnLayoutChangeListener(capture(captor))
+ captor.value.onLayoutChange(clock, 0, 0, width, 0, 0, 0, 0, 0)
+
+ verify(carrierGroup).setPaddingRelative(514, 0, 0, 0)
+ }
+
private fun View.executeLayoutChange(
left: Int,
top: Int,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
index b4c8f98..2bf2a81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
@@ -1,5 +1,7 @@
package com.android.systemui.shade
+import android.animation.Animator
+import android.animation.ValueAnimator
import android.app.StatusBarManager
import android.content.Context
import android.testing.AndroidTestingRunner
@@ -30,6 +32,7 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -37,13 +40,14 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
+import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -75,6 +79,7 @@
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
var viewVisibility = View.GONE
+ var viewAlpha = 1f
private lateinit var mLargeScreenShadeHeaderController: LargeScreenShadeHeaderController
private lateinit var carrierIconSlots: List<String>
@@ -101,6 +106,13 @@
null
}
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
+
+ whenever(view.setAlpha(anyFloat())).then {
+ viewAlpha = it.arguments[0] as Float
+ null
+ }
+ whenever(view.alpha).thenAnswer { _ -> viewAlpha }
+
whenever(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
@@ -155,6 +167,16 @@
}
@Test
+ fun alphaChangesUpdateVisibility() {
+ makeShadeVisible()
+ mLargeScreenShadeHeaderController.shadeExpandedFraction = 0f
+ assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
+
+ mLargeScreenShadeHeaderController.shadeExpandedFraction = 1f
+ assertThat(viewVisibility).isEqualTo(View.VISIBLE)
+ }
+
+ @Test
fun singleCarrier_enablesCarrierIconsInStatusIcons() {
whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true)
@@ -239,6 +261,67 @@
}
@Test
+ fun testShadeExpanded_true_alpha_zero_invisible() {
+ view.alpha = 0f
+ mLargeScreenShadeHeaderController.largeScreenActive = true
+ mLargeScreenShadeHeaderController.qsVisible = true
+
+ assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
+ }
+
+ @Test
+ fun animatorCallsUpdateVisibilityOnUpdate() {
+ val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+ whenever(view.animate()).thenReturn(animator)
+
+ mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, 0L)
+
+ val updateCaptor = argumentCaptor<ValueAnimator.AnimatorUpdateListener>()
+ verify(animator).setUpdateListener(capture(updateCaptor))
+
+ mLargeScreenShadeHeaderController.largeScreenActive = true
+ mLargeScreenShadeHeaderController.qsVisible = true
+
+ view.alpha = 1f
+ updateCaptor.value.onAnimationUpdate(mock())
+
+ assertThat(viewVisibility).isEqualTo(View.VISIBLE)
+
+ view.alpha = 0f
+ updateCaptor.value.onAnimationUpdate(mock())
+
+ assertThat(viewVisibility).isEqualTo(View.INVISIBLE)
+ }
+
+ @Test
+ fun animatorListenersClearedAtEnd() {
+ val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+ whenever(view.animate()).thenReturn(animator)
+
+ mLargeScreenShadeHeaderController.startCustomizingAnimation(show = true, 0L)
+ val listenerCaptor = argumentCaptor<Animator.AnimatorListener>()
+ verify(animator).setListener(capture(listenerCaptor))
+
+ listenerCaptor.value.onAnimationEnd(mock())
+ verify(animator).setListener(null)
+ verify(animator).setUpdateListener(null)
+ }
+
+ @Test
+ fun animatorListenersClearedOnCancel() {
+ val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
+ whenever(view.animate()).thenReturn(animator)
+
+ mLargeScreenShadeHeaderController.startCustomizingAnimation(show = true, 0L)
+ val listenerCaptor = argumentCaptor<Animator.AnimatorListener>()
+ verify(animator).setListener(capture(listenerCaptor))
+
+ listenerCaptor.value.onAnimationCancel(mock())
+ verify(animator).setListener(null)
+ verify(animator).setUpdateListener(null)
+ }
+
+ @Test
fun demoMode_attachDemoMode() {
val cb = argumentCaptor<DemoMode>()
verify(demoModeController).addCallback(capture(cb))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 65b2ac0..28f7edf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -97,16 +97,23 @@
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.common.ui.view.LongPressHandlingView;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -193,7 +200,7 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class NotificationPanelViewControllerTest extends SysuiTestCase {
private static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -293,8 +300,15 @@
@Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
@Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
@Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+ @Mock private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel;
+ @Mock private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
+ @Mock private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
+
@Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ @Mock private KeyguardInteractor mKeyguardInteractor;
+ @Mock private KeyguardLongPressViewModel mKeyuardLongPressViewModel;
@Mock private CoroutineDispatcher mMainDispatcher;
+ @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock private MotionEvent mDownMotionEvent;
@Captor
private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
@@ -450,6 +464,9 @@
mMainHandler = new Handler(Looper.getMainLooper());
+ when(mView.requireViewById(R.id.keyguard_long_press))
+ .thenReturn(mock(LongPressHandlingView.class));
+
mNotificationPanelViewController = new NotificationPanelViewController(
mView,
mMainHandler,
@@ -511,11 +528,17 @@
systemClock,
mKeyguardBottomAreaViewModel,
mKeyguardBottomAreaInteractor,
+ mAlternateBouncerInteractor,
mDreamingToLockscreenTransitionViewModel,
mOccludedToLockscreenTransitionViewModel,
+ mLockscreenToDreamingTransitionViewModel,
+ mGoneToDreamingTransitionViewModel,
+ mLockscreenToOccludedTransitionViewModel,
mMainDispatcher,
mKeyguardTransitionInteractor,
- mDumpManager);
+ mDumpManager,
+ mKeyuardLongPressViewModel,
+ mKeyguardInteractor);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
index bdafc7d..c915502a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
@@ -13,8 +13,11 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.fragments.FragmentHostManager
+import com.android.systemui.fragments.FragmentService
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
+import com.android.systemui.plugins.qs.QS
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.util.concurrency.FakeExecutor
@@ -29,6 +32,7 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.eq
@@ -69,6 +73,10 @@
private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
@Mock
private lateinit var featureFlags: FeatureFlags
+ @Mock
+ private lateinit var fragmentService: FragmentService
+ @Mock
+ private lateinit var fragmentHostManager: FragmentHostManager
@Captor
lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
@Captor
@@ -77,6 +85,8 @@
lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
@Captor
lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet>
+ @Captor
+ lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener>
private lateinit var controller: NotificationsQSContainerController
private lateinit var navigationModeCallback: ModeChangedListener
@@ -91,8 +101,10 @@
mContext.ensureTestableResources()
whenever(notificationsQSContainer.context).thenReturn(mContext)
whenever(notificationsQSContainer.resources).thenReturn(mContext.resources)
+ whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager)
fakeSystemClock = FakeSystemClock()
delayableExecutor = FakeExecutor(fakeSystemClock)
+
controller = NotificationsQSContainerController(
notificationsQSContainer,
navigationModeController,
@@ -100,6 +112,7 @@
largeScreenShadeHeaderController,
shadeExpansionStateManager,
featureFlags,
+ fragmentService,
delayableExecutor
)
@@ -114,9 +127,10 @@
doNothing().`when`(notificationsQSContainer)
.setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture())
-
+ doNothing().`when`(notificationsQSContainer)
+ .addOnAttachStateChangeListener(attachStateListenerCaptor.capture())
controller.init()
- controller.onViewAttached()
+ attachStateListenerCaptor.value.onViewAttachedToWindow(notificationsQSContainer)
navigationModeCallback = navigationModeCaptor.value
taskbarVisibilityCallback = taskbarVisibilityCaptor.value
@@ -385,6 +399,7 @@
largeScreenShadeHeaderController,
shadeExpansionStateManager,
featureFlags,
+ fragmentService,
delayableExecutor
)
controller.updateConstraints()
@@ -426,6 +441,17 @@
verify(largeScreenShadeHeaderController).startCustomizingAnimation(false, 100L)
}
+ @Test
+ fun testTagListenerAdded() {
+ verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(notificationsQSContainer))
+ }
+
+ @Test
+ fun testTagListenerRemoved() {
+ attachStateListenerCaptor.value.onViewDetachedFromWindow(notificationsQSContainer)
+ verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(notificationsQSContainer))
+ }
+
private fun disableSplitShade() {
setSplitShadeEnabled(false)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 08a9c96..526dc8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -46,11 +46,14 @@
import androidx.test.filters.SmallTest;
import com.android.internal.colorextraction.ColorExtractor;
+import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -68,6 +71,8 @@
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
+import java.util.List;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
@@ -91,13 +96,21 @@
@Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
@Mock private ShadeWindowLogger mShadeWindowLogger;
@Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
+ @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
-
+ private float mPreferredRefreshRate = -1;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ // Preferred refresh rate is equal to the first displayMode's refresh rate
+ mPreferredRefreshRate = mContext.getDisplay().getSupportedModes()[0].getRefreshRate();
+ overrideResource(
+ R.integer.config_keyguardRefreshRate,
+ (int) mPreferredRefreshRate
+ );
+
when(mDozeParameters.getAlwaysOn()).thenReturn(true);
when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
@@ -117,6 +130,7 @@
mNotificationShadeWindowController.attach();
verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
+ verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt());
}
@Test
@@ -334,4 +348,59 @@
assertThat(mLayoutParameters.getValue().screenOrientation)
.isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
}
+
+ @Test
+ public void udfpsEnrolled_minAndMaxRefreshRateSetToPreferredRefreshRate() {
+ // GIVEN udfps is enrolled
+ when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+ // WHEN keyguard is showing
+ setKeyguardShowing();
+
+ // THEN min and max refresh rate is set to the preferredRefreshRate
+ verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+ final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+ final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+ assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+ assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(mPreferredRefreshRate);
+ }
+
+ @Test
+ public void udfpsNotEnrolled_refreshRateUnset() {
+ // GIVEN udfps is NOT enrolled
+ when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
+
+ // WHEN keyguard is showing
+ setKeyguardShowing();
+
+ // THEN min and max refresh rate aren't set (set to 0)
+ verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+ final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+ final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+ assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+ assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+ }
+
+ @Test
+ public void keyguardNotShowing_refreshRateUnset() {
+ // GIVEN UDFPS is enrolled
+ when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
+
+ // WHEN keyguard is NOT showing
+ mNotificationShadeWindowController.setKeyguardShowing(false);
+
+ // THEN min and max refresh rate aren't set (set to 0)
+ verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture());
+ final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues();
+ final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1);
+ assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0);
+ assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0);
+ }
+
+ private void setKeyguardShowing() {
+ mNotificationShadeWindowController.setKeyguardShowing(true);
+ mNotificationShadeWindowController.setKeyguardGoingAway(false);
+ mNotificationShadeWindowController.setKeyguardFadingAway(false);
+ mStateListener.getValue().onStateChanged(StatusBarState.KEYGUARD);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index c3207c2..e5d5e3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -30,6 +30,8 @@
import com.android.systemui.dock.DockManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -43,13 +45,16 @@
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
+import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -97,10 +102,12 @@
private lateinit var pulsingGestureListener: PulsingGestureListener
@Mock
private lateinit var notificationInsetsController: NotificationInsetsController
+ @Mock
+ private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
- @Mock lateinit var keyguardBouncerContainer: ViewGroup
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardHostViewController: KeyguardHostViewController
+ @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
private lateinit var interactionEventHandler: InteractionEventHandler
@@ -111,6 +118,12 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(view.bottom).thenReturn(VIEW_BOTTOM)
+ whenever(view.findViewById<ViewGroup>(R.id.keyguard_bouncer_container))
+ .thenReturn(mock(ViewGroup::class.java))
+ whenever(keyguardBouncerComponentFactory.create(any(ViewGroup::class.java)))
+ .thenReturn(keyguardBouncerComponent)
+ whenever(keyguardBouncerComponent.keyguardHostViewController)
+ .thenReturn(keyguardHostViewController)
underTest = NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
FalsingCollectorFake(),
@@ -132,7 +145,9 @@
pulsingGestureListener,
featureFlags,
keyguardBouncerViewModel,
- keyguardBouncerComponentFactory
+ keyguardBouncerComponentFactory,
+ alternateBouncerInteractor,
+ keyguardTransitionInteractor,
)
underTest.setupExpandedStatusBar()
@@ -268,6 +283,7 @@
@Test
fun testGetBouncerContainer() {
+ Mockito.clearInvocations(view)
underTest.bouncerContainer
verify(view).findViewById<ViewGroup>(R.id.keyguard_bouncer_container)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 4bf00c4..5cc3ef1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -29,9 +29,11 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
+import com.android.keyguard.KeyguardHostViewController;
import com.android.keyguard.LockIconViewController;
import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.R;
@@ -40,6 +42,8 @@
import com.android.systemui.dock.DockManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.statusbar.DragDownHelper;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -92,7 +96,11 @@
@Mock private FeatureFlags mFeatureFlags;
@Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel;
@Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
+ @Mock private KeyguardBouncerComponent mKeyguardBouncerComponent;
+ @Mock private KeyguardHostViewController mKeyguardHostViewController;
@Mock private NotificationInsetsController mNotificationInsetsController;
+ @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
+ @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler>
mInteractionEventHandlerCaptor;
@@ -106,6 +114,12 @@
when(mView.findViewById(R.id.notification_stack_scroller))
.thenReturn(mNotificationStackScrollLayout);
+ when(mView.findViewById(R.id.keyguard_bouncer_container)).thenReturn(mock(ViewGroup.class));
+ when(mKeyguardBouncerComponentFactory.create(any(ViewGroup.class))).thenReturn(
+ mKeyguardBouncerComponent);
+ when(mKeyguardBouncerComponent.getKeyguardHostViewController()).thenReturn(
+ mKeyguardHostViewController);
+
when(mStatusBarStateController.isDozing()).thenReturn(false);
mDependency.injectTestDependency(ShadeController.class, mShadeController);
@@ -132,7 +146,9 @@
mPulsingGestureListener,
mFeatureFlags,
mKeyguardBouncerViewModel,
- mKeyguardBouncerComponentFactory
+ mKeyguardBouncerComponentFactory,
+ mAlternateBouncerInteractor,
+ mKeyguardTransitionInteractor
);
mController.setupExpandedStatusBar();
mController.setDragDownHelper(mDragDownHelper);
@@ -155,7 +171,7 @@
// WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
// THEN we should intercept touch
@@ -168,7 +184,7 @@
// WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept
when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(false);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false);
when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
// THEN we shouldn't intercept touch
@@ -181,7 +197,7 @@
// WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
// THEN we should handle the touch
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 3e769e9..76aa08a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.tuner.TunerService
import com.android.systemui.tuner.TunerService.Tunable
@@ -69,6 +70,8 @@
private lateinit var statusBarStateController: StatusBarStateController
@Mock
private lateinit var shadeLogger: ShadeLogger
+ @Mock
+ private lateinit var userTracker: UserTracker
private lateinit var tunableCaptor: ArgumentCaptor<Tunable>
private lateinit var underTest: PulsingGestureListener
@@ -85,6 +88,7 @@
ambientDisplayConfiguration,
statusBarStateController,
shadeLogger,
+ userTracker,
tunerService,
dumpManager
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
index 5a62cc1..ae1c8cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
@@ -1,21 +1,17 @@
package com.android.systemui.shared.regionsampling
-import android.graphics.Rect
+import android.app.WallpaperManager
import android.testing.AndroidTestingRunner
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper
import java.io.PrintWriter
import java.util.concurrent.Executor
-import org.junit.Assert
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.clearInvocations
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@@ -28,9 +24,8 @@
@Mock private lateinit var sampledView: View
@Mock private lateinit var mainExecutor: Executor
@Mock private lateinit var bgExecutor: Executor
- @Mock private lateinit var regionSampler: RegionSamplingHelper
@Mock private lateinit var pw: PrintWriter
- @Mock private lateinit var callback: RegionSamplingHelper.SamplingCallback
+ @Mock private lateinit var wallpaperManager: WallpaperManager
private lateinit var mRegionSampler: RegionSampler
private var updateFun: UpdateColorCallback = {}
@@ -38,65 +33,18 @@
@Before
fun setUp() {
whenever(sampledView.isAttachedToWindow).thenReturn(true)
- whenever(regionSampler.callback).thenReturn(this@RegionSamplerTest.callback)
mRegionSampler =
- object : RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun) {
- override fun createRegionSamplingHelper(
- sampledView: View,
- callback: RegionSamplingHelper.SamplingCallback,
- mainExecutor: Executor?,
- bgExecutor: Executor?
- ): RegionSamplingHelper {
- return this@RegionSamplerTest.regionSampler
- }
- }
+ RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun, wallpaperManager)
}
@Test
fun testStartRegionSampler() {
mRegionSampler.startRegionSampler()
-
- verify(regionSampler).start(Rect(0, 0, 0, 0))
- }
-
- @Test
- fun testStopRegionSampler() {
- mRegionSampler.stopRegionSampler()
-
- verify(regionSampler).stop()
}
@Test
fun testDump() {
mRegionSampler.dump(pw)
-
- verify(regionSampler).dump(pw)
- }
-
- @Test
- fun testUpdateColorCallback() {
- regionSampler.callback.onRegionDarknessChanged(false)
- verify(regionSampler.callback).onRegionDarknessChanged(false)
- clearInvocations(regionSampler.callback)
- regionSampler.callback.onRegionDarknessChanged(true)
- verify(regionSampler.callback).onRegionDarknessChanged(true)
- }
-
- @Test
- fun testFlagFalse() {
- mRegionSampler =
- object : RegionSampler(sampledView, mainExecutor, bgExecutor, false, updateFun) {
- override fun createRegionSamplingHelper(
- sampledView: View,
- callback: RegionSamplingHelper.SamplingCallback,
- mainExecutor: Executor?,
- bgExecutor: Executor?
- ): RegionSamplingHelper {
- return this@RegionSamplerTest.regionSampler
- }
- }
-
- Assert.assertEquals(mRegionSampler.regionSampler, null)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
new file mode 100644
index 0000000..5fb1e79
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.smartspace
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.smartspace.config.BcSmartspaceConfigProvider
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BcSmartspaceConfigProviderTest : SysuiTestCase() {
+ @Mock private lateinit var featureFlags: FeatureFlags
+
+ private lateinit var configProvider: BcSmartspaceConfigProvider
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ configProvider = BcSmartspaceConfigProvider(featureFlags)
+ }
+
+ @Test
+ fun isDefaultDateWeatherDisabled_flagIsTrue_returnsTrue() {
+ whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+
+ assertTrue(configProvider.isDefaultDateWeatherDisabled)
+ }
+
+ @Test
+ fun isDefaultDateWeatherDisabled_flagIsFalse_returnsFalse() {
+ whenever(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
+
+ assertFalse(configProvider.isDefaultDateWeatherDisabled)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
index 001e1f4..c5432c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -27,6 +27,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dreams.smartspace.DreamSmartspaceController
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
import com.android.systemui.plugins.FalsingManager
@@ -94,6 +95,8 @@
private class TestView(context: Context?) : View(context), SmartspaceView {
override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {}
+ override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {}
+
override fun setPrimaryTextColor(color: Int) {}
override fun setIsDreaming(isDreaming: Boolean) {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
index d29e9a6..fa7d869 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
@@ -20,8 +20,6 @@
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.smartspace.preconditions.LockscreenPrecondition
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.util.concurrency.Execution
@@ -41,9 +39,6 @@
@TestableLooper.RunWithLooper
class LockscreenPreconditionTest : SysuiTestCase() {
@Mock
- private lateinit var featureFlags: FeatureFlags
-
- @Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock
@@ -64,10 +59,7 @@
fun testFullyEnabled() {
`when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
`when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
- `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE))
- .thenReturn(true)
- val precondition = LockscreenPrecondition(featureFlags, deviceProvisionedController,
- execution)
+ val precondition = LockscreenPrecondition(deviceProvisionedController, execution)
precondition.addListener(listener)
`verify`(listener).onCriteriaChanged()
@@ -81,10 +73,8 @@
fun testProvisioning() {
`when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
`when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(false)
- `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE))
- .thenReturn(true)
val precondition =
- LockscreenPrecondition(featureFlags, deviceProvisionedController, execution)
+ LockscreenPrecondition(deviceProvisionedController, execution)
precondition.addListener(listener)
verify(listener).onCriteriaChanged()
@@ -109,10 +99,8 @@
fun testUserSetup() {
`when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
`when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
- `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE))
- .thenReturn(true)
val precondition =
- LockscreenPrecondition(featureFlags, deviceProvisionedController, execution)
+ LockscreenPrecondition(deviceProvisionedController, execution)
precondition.addListener(listener)
verify(listener).onCriteriaChanged()
@@ -129,4 +117,4 @@
verify(listener).onCriteriaChanged()
assertThat(precondition.conditionsMet()).isTrue()
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 8aaa181..e68d3b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -45,6 +45,7 @@
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import org.junit.After;
@@ -62,12 +63,14 @@
};
private CommandQueue mCommandQueue;
+
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private Callbacks mCallbacks;
private static final int SECONDARY_DISPLAY = 1;
@Before
public void setup() {
- mCommandQueue = new CommandQueue(mContext);
+ mCommandQueue = new CommandQueue(mContext, mDisplayTracker);
mCallbacks = mock(Callbacks.class);
mCommandQueue.addCallback(mCallbacks);
verify(mCallbacks).disable(anyInt(), eq(0), eq(0), eq(false));
@@ -415,7 +418,7 @@
@Test
public void testOnDisplayRemoved() {
- mCommandQueue.onDisplayRemoved(SECONDARY_DISPLAY);
+ mDisplayTracker.triggerOnDisplayRemoved(SECONDARY_DISPLAY);
waitForIdleSync();
verify(mCallbacks).onDisplayRemoved(eq(SECONDARY_DISPLAY));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index d2dd433..dffa566 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -58,6 +58,7 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.app.AlarmManager;
import android.app.Instrumentation;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyResourcesManager;
@@ -99,6 +100,7 @@
import com.android.systemui.keyguard.KeyguardIndication;
import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController;
import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -177,9 +179,13 @@
@Mock
private FaceHelpMessageDeferral mFaceHelpMessageDeferral;
@Mock
+ private AlternateBouncerInteractor mAlternateBouncerInteractor;
+ @Mock
private ScreenLifecycle mScreenLifecycle;
@Mock
private AuthController mAuthController;
+ @Mock
+ private AlarmManager mAlarmManager;
@Captor
private ArgumentCaptor<DockManager.AlignmentStateListener> mAlignmentListener;
@Captor
@@ -273,7 +279,10 @@
mUserManager, mExecutor, mExecutor, mFalsingManager,
mAuthController, mLockPatternUtils, mScreenLifecycle,
mKeyguardBypassController, mAccessibilityManager,
- mFaceHelpMessageDeferral, mock(KeyguardLogger.class));
+ mFaceHelpMessageDeferral, mock(KeyguardLogger.class),
+ mAlternateBouncerInteractor,
+ mAlarmManager
+ );
mController.init();
mController.setIndicationArea(mIndicationArea);
verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 3d11ced..702f278 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -244,6 +244,14 @@
}
@Test
+ fun testGoToLockedShadeAlwaysCreatesQSAnimationInSplitShade() {
+ enableSplitShade()
+ transitionController.goToLockedShade(null, needsQSAnimation = true)
+ verify(notificationPanelController).animateToFullShade(anyLong())
+ assertNotNull(transitionController.dragDownAnimator)
+ }
+
+ @Test
fun testDragDownAmountDoesntCallOutInLockedDownShade() {
whenever(nsslController.isInLockedDownShade).thenReturn(true)
transitionController.dragDownAmount = 10f
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 5124eb9..e6f272b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -37,6 +37,7 @@
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -152,4 +153,18 @@
// and cause us to drop a frame during the LOCKSCREEN_TRANSITION_FROM_AOD CUJ.
assertEquals(0.99f, controller.dozeAmount, 0.009f)
}
+
+ @Test
+ fun testSetDreamState_invokesCallback() {
+ val listener = mock(StatusBarStateController.StateListener::class.java)
+ controller.addCallback(listener)
+
+ controller.setIsDreaming(true)
+ verify(listener).onDreamingChanged(true)
+
+ Mockito.clearInvocations(listener)
+
+ controller.setIsDreaming(false)
+ verify(listener).onDreamingChanged(false)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt
index ea06647..a9c3d5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt
@@ -6,6 +6,7 @@
import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -17,6 +18,7 @@
class GenericGestureDetectorTest : SysuiTestCase() {
private lateinit var gestureDetector: TestGestureDetector
+ private val displayTracker = FakeDisplayTracker(mContext)
@Before
fun setUp() {
@@ -101,12 +103,21 @@
gestureDetector.addOnGestureDetectedCallback("tag2"){}
gestureDetector.removeOnGestureDetectedCallback("tag")
- gestureDetector.onInputEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, CORRECT_X, 0f, 0))
+ gestureDetector.onInputEvent(
+ MotionEvent.obtain(
+ 0,
+ 0,
+ MotionEvent.ACTION_DOWN,
+ CORRECT_X,
+ 0f,
+ 0
+ )
+ )
assertThat(oldCallbackNotified).isFalse()
}
- inner class TestGestureDetector : GenericGestureDetector("fakeTag") {
+ inner class TestGestureDetector : GenericGestureDetector("fakeTag", displayTracker) {
var isGestureListening = false
override fun onInputEvent(ev: InputEvent) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index ddcf59e..0a576de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -36,6 +36,7 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceConfigPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
@@ -112,9 +113,18 @@
private lateinit var handler: Handler
@Mock
+ private lateinit var datePlugin: BcSmartspaceDataPlugin
+
+ @Mock
+ private lateinit var weatherPlugin: BcSmartspaceDataPlugin
+
+ @Mock
private lateinit var plugin: BcSmartspaceDataPlugin
@Mock
+ private lateinit var configPlugin: BcSmartspaceConfigPlugin
+
+ @Mock
private lateinit var controllerListener: SmartspaceTargetListener
@Captor
@@ -148,6 +158,8 @@
KeyguardBypassController.OnBypassStateChangedListener
private lateinit var deviceProvisionedListener: DeviceProvisionedListener
+ private lateinit var dateSmartspaceView: SmartspaceView
+ private lateinit var weatherSmartspaceView: SmartspaceView
private lateinit var smartspaceView: SmartspaceView
private val clock = FakeSystemClock()
@@ -173,18 +185,24 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(featureFlags.isEnabled(Flags.SMARTSPACE)).thenReturn(true)
+ // Todo(b/261760571): flip the flag value here when feature is launched, and update relevant
+ // tests.
+ `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
`when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING))
.thenReturn(fakePrivateLockscreenSettingUri)
`when`(secureSettings.getUriFor(NOTIF_ON_LOCKSCREEN_SETTING))
.thenReturn(fakeNotifOnLockscreenSettingUri)
`when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession)
+ `when`(datePlugin.getView(any())).thenReturn(
+ createDateSmartspaceView(), createDateSmartspaceView())
+ `when`(weatherPlugin.getView(any())).thenReturn(
+ createWeatherSmartspaceView(), createWeatherSmartspaceView())
`when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView())
`when`(userTracker.userProfiles).thenReturn(userList)
`when`(statusBarStateController.dozeAmount).thenReturn(0.5f)
- `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
- `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
setActiveUser(userHandlePrimary)
setAllowPrivateNotifications(userHandlePrimary, true)
@@ -209,7 +227,10 @@
executor,
bgExecutor,
handler,
- Optional.of(plugin)
+ Optional.of(datePlugin),
+ Optional.of(weatherPlugin),
+ Optional.of(plugin),
+ Optional.of(configPlugin),
)
verify(deviceProvisionedController).addCallback(capture(deviceProvisionedCaptor))
@@ -217,21 +238,21 @@
}
@Test(expected = RuntimeException::class)
- fun testThrowsIfFlagIsDisabled() {
+ fun testBuildAndConnectWeatherView_throwsIfDecouplingDisabled() {
// GIVEN the feature flag is disabled
- `when`(featureFlags.isEnabled(Flags.SMARTSPACE)).thenReturn(false)
+ `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(false)
// WHEN we try to build the view
- controller.buildAndConnectView(fakeParent)
+ controller.buildAndConnectWeatherView(fakeParent)
// THEN an exception is thrown
}
@Test
- fun connectOnlyAfterDeviceIsProvisioned() {
+ fun testBuildAndConnectView_connectsOnlyAfterDeviceIsProvisioned() {
// GIVEN an unprovisioned device and an attempt to connect
- `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(false)
- `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(false)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(false)
+ `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
// WHEN a connection attempt is made and view is attached
val view = controller.buildAndConnectView(fakeParent)
@@ -241,8 +262,8 @@
verify(smartspaceManager, never()).createSmartspaceSession(any())
// WHEN it does become provisioned
- `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
- `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
deviceProvisionedListener.onUserSetupChanged()
// THEN the session is created
@@ -252,7 +273,7 @@
}
@Test
- fun testListenersAreRegistered() {
+ fun testAddListener_registersListenersForPlugin() {
// GIVEN a listener is added after a session is created
connectSession()
@@ -261,10 +282,13 @@
// THEN the listener is registered to the underlying plugin
verify(plugin).registerListener(controllerListener)
+ // The listener is registered only for the plugin, not the date, or weather plugin.
+ verify(datePlugin, never()).registerListener(any())
+ verify(weatherPlugin, never()).registerListener(any())
}
@Test
- fun testEarlyRegisteredListenersAreAttachedAfterConnected() {
+ fun testAddListener_earlyRegisteredListenersAreAttachedAfterConnected() {
// GIVEN a listener that is registered before the session is created
controller.addListener(controllerListener)
@@ -273,10 +297,13 @@
// THEN the listener is subsequently registered
verify(plugin).registerListener(controllerListener)
+ // The listener is registered only for the plugin, not the date, or the weather plugin.
+ verify(datePlugin, never()).registerListener(any())
+ verify(weatherPlugin, never()).registerListener(any())
}
@Test
- fun testEmptyListIsEmittedAndNotifierRemovedAfterDisconnect() {
+ fun testDisconnect_emitsEmptyListAndRemovesNotifier() {
// GIVEN a registered listener on an active session
connectSession()
clearInvocations(plugin)
@@ -288,10 +315,13 @@
// THEN the listener receives an empty list of targets and unregisters the notifier
verify(plugin).onTargetsAvailable(emptyList())
verify(plugin).registerSmartspaceEventNotifier(null)
+ verify(weatherPlugin).onTargetsAvailable(emptyList())
+ verify(weatherPlugin).registerSmartspaceEventNotifier(null)
+ verify(datePlugin).registerSmartspaceEventNotifier(null)
}
@Test
- fun testUserChangeReloadsSmartspace() {
+ fun testUserChange_reloadsSmartspace() {
// GIVEN a connected smartspace session
connectSession()
@@ -303,7 +333,7 @@
}
@Test
- fun testSettingsChangeReloadsSmartspace() {
+ fun testSettingsChange_reloadsSmartspace() {
// GIVEN a connected smartspace session
connectSession()
@@ -315,7 +345,7 @@
}
@Test
- fun testThemeChangeUpdatesTextColor() {
+ fun testThemeChange_updatesTextColor() {
// GIVEN a connected smartspace session
connectSession()
@@ -327,7 +357,23 @@
}
@Test
- fun testDozeAmountChangeUpdatesView() {
+ fun testThemeChange_ifDecouplingEnabled_updatesTextColor() {
+ `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+
+ // GIVEN a connected smartspace session
+ connectSession()
+
+ // WHEN the theme changes
+ configChangeListener.onThemeChanged()
+
+ // We update the new text color to match the wallpaper color
+ verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(smartspaceView).setPrimaryTextColor(anyInt())
+ }
+
+ @Test
+ fun testDozeAmountChange_updatesView() {
// GIVEN a connected smartspace session
connectSession()
@@ -339,7 +385,23 @@
}
@Test
- fun testKeyguardBypassEnabledUpdatesView() {
+ fun testDozeAmountChange_ifDecouplingEnabled_updatesViews() {
+ `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+
+ // GIVEN a connected smartspace session
+ connectSession()
+
+ // WHEN the doze amount changes
+ statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f)
+
+ // We pass that along to the view
+ verify(dateSmartspaceView).setDozeAmount(0.7f)
+ verify(weatherSmartspaceView).setDozeAmount(0.7f)
+ verify(smartspaceView).setDozeAmount(0.7f)
+ }
+
+ @Test
+ fun testKeyguardBypassEnabled_updatesView() {
// GIVEN a connected smartspace session
connectSession()
`when`(keyguardBypassController.bypassEnabled).thenReturn(true)
@@ -434,6 +496,29 @@
}
@Test
+ fun testSessionListener_ifDecouplingEnabled_weatherTargetIsFilteredOut() {
+ `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+ connectSession()
+
+ // WHEN we receive a list of targets
+ val targets = listOf(
+ makeTarget(1, userHandlePrimary, isSensitive = true),
+ makeTarget(2, userHandlePrimary),
+ makeTarget(3, userHandleManaged),
+ makeTarget(4, userHandlePrimary, featureType = SmartspaceTarget.FEATURE_WEATHER)
+ )
+
+ sessionListener.onTargetsAvailable(targets)
+
+ // THEN all non-sensitive content is still shown
+ verify(plugin).onTargetsAvailable(eq(listOf(targets[0], targets[1], targets[2])))
+ // No filtering is applied for the weather plugin
+ verify(weatherPlugin).onTargetsAvailable(eq(targets))
+ // No targets needed for the date plugin
+ verify(datePlugin, never()).onTargetsAvailable(any())
+ }
+
+ @Test
fun testSettingsAreReloaded() {
// GIVEN a connected session where the privacy settings later flip to false
connectSession()
@@ -520,6 +605,17 @@
verify(smartspaceManager, never()).createSmartspaceSession(any())
verify(smartspaceView2).setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
verify(smartspaceView2).registerDataProvider(plugin)
+ verify(smartspaceView2).registerConfigProvider(configPlugin)
+ }
+
+ @Test
+ fun testWeatherViewUsesSameSession() {
+ `when`(featureFlags.isEnabled(Flags.SMARTSPACE_DATE_WEATHER_DECOUPLED)).thenReturn(true)
+ // GIVEN a connected session
+ connectSession()
+
+ // No checks is needed here, since connectSession() already checks internally that
+ // createSmartspaceSession is invoked only once.
}
@Test
@@ -539,8 +635,8 @@
fun testConnectAttemptBeforeInitializationShouldNotCreateSession() {
// GIVEN an uninitalized smartspaceView
// WHEN the device is provisioned
- `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true)
- `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
deviceProvisionedListener.onDeviceProvisionedChanged()
// THEN no calls to createSmartspaceSession should occur
@@ -550,17 +646,46 @@
}
private fun connectSession() {
+ if (controller.isDateWeatherDecoupled()) {
+ val dateView = controller.buildAndConnectDateView(fakeParent)
+ dateSmartspaceView = dateView as SmartspaceView
+ fakeParent.addView(dateView)
+ controller.stateChangeListener.onViewAttachedToWindow(dateView)
+
+ verify(dateSmartspaceView).setUiSurface(
+ BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+ verify(dateSmartspaceView).registerDataProvider(datePlugin)
+
+ verify(dateSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(dateSmartspaceView).setDozeAmount(0.5f)
+
+ val weatherView = controller.buildAndConnectWeatherView(fakeParent)
+ weatherSmartspaceView = weatherView as SmartspaceView
+ fakeParent.addView(weatherView)
+ controller.stateChangeListener.onViewAttachedToWindow(weatherView)
+
+ verify(weatherSmartspaceView).setUiSurface(
+ BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
+ verify(weatherSmartspaceView).registerDataProvider(weatherPlugin)
+
+ verify(weatherSmartspaceView).setPrimaryTextColor(anyInt())
+ verify(weatherSmartspaceView).setDozeAmount(0.5f)
+ }
+
val view = controller.buildAndConnectView(fakeParent)
smartspaceView = view as SmartspaceView
-
+ fakeParent.addView(view)
controller.stateChangeListener.onViewAttachedToWindow(view)
verify(smartspaceView).setUiSurface(BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
verify(smartspaceView).registerDataProvider(plugin)
+ verify(smartspaceView).registerConfigProvider(configPlugin)
verify(smartspaceSession)
.addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor))
sessionListener = sessionListenerCaptor.value
+ verify(smartspaceManager).createSmartspaceSession(any())
+
verify(userTracker).addCallback(capture(userTrackerCaptor), any())
userListener = userTrackerCaptor.value
@@ -585,9 +710,12 @@
verify(smartspaceView).setPrimaryTextColor(anyInt())
verify(smartspaceView).setDozeAmount(0.5f)
- clearInvocations(view)
- fakeParent.addView(view)
+ if (controller.isDateWeatherDecoupled()) {
+ clearInvocations(dateSmartspaceView)
+ clearInvocations(weatherSmartspaceView)
+ }
+ clearInvocations(smartspaceView)
}
private fun setActiveUser(userHandle: UserHandle) {
@@ -633,11 +761,70 @@
).thenReturn(if (value) 1 else 0)
}
+ // Separate function for the date view, which implements a specific subset of all functions.
+ private fun createDateSmartspaceView(): SmartspaceView {
+ return spy(object : View(context), SmartspaceView {
+ override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+ }
+
+ override fun setPrimaryTextColor(color: Int) {
+ }
+
+ override fun setIsDreaming(isDreaming: Boolean) {
+ }
+
+ override fun setUiSurface(uiSurface: String) {
+ }
+
+ override fun setDozeAmount(amount: Float) {
+ }
+
+ override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+ }
+
+ override fun setFalsingManager(falsingManager: FalsingManager?) {
+ }
+
+ override fun setDnd(image: Drawable?, description: String?) {
+ }
+
+ override fun setNextAlarm(image: Drawable?, description: String?) {
+ }
+ })
+ }
+ // Separate function for the weather view, which implements a specific subset of all functions.
+ private fun createWeatherSmartspaceView(): SmartspaceView {
+ return spy(object : View(context), SmartspaceView {
+ override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
+ }
+
+ override fun setPrimaryTextColor(color: Int) {
+ }
+
+ override fun setIsDreaming(isDreaming: Boolean) {
+ }
+
+ override fun setUiSurface(uiSurface: String) {
+ }
+
+ override fun setDozeAmount(amount: Float) {
+ }
+
+ override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {
+ }
+
+ override fun setFalsingManager(falsingManager: FalsingManager?) {
+ }
+ })
+ }
private fun createSmartspaceView(): SmartspaceView {
return spy(object : View(context), SmartspaceView {
override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {
}
+ override fun registerConfigProvider(plugin: BcSmartspaceConfigPlugin?) {
+ }
+
override fun setPrimaryTextColor(color: Int) {
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 09f8a10..a869038 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -39,7 +39,6 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
@@ -137,7 +136,6 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
allowTestableLooperAsMainThread();
- when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
mListBuilder = new ShadeListBuilder(
mDumpManager,
@@ -1998,29 +1996,7 @@
}
@Test
- public void testActiveOrdering_withLegacyStability() {
- when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
- assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
- assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
- assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
- assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
- assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
- }
-
- @Test
- public void testStableOrdering_withLegacyStability() {
- when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
- mStabilityManager.setAllowEntryReordering(false);
- assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
- assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
- assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
- assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
- assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
- }
-
- @Test
public void testStableOrdering() {
- when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
mStabilityManager.setAllowEntryReordering(false);
// No input or output
assertOrder("", "", "", true);
@@ -2076,7 +2052,6 @@
@Test
public void testActiveOrdering() {
- when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
@@ -2133,7 +2108,6 @@
@Test
public void stableOrderingDisregardedWithSectionChange() {
- when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
// GIVEN the first sectioner's packages can be changed from run-to-run
List<String> mutableSectionerPackages = new ArrayList<>();
mutableSectionerPackages.add(PACKAGE_1);
@@ -2229,49 +2203,7 @@
}
@Test
- public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
- when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
-
- // GIVEN a notification group is on screen
- mStabilityManager.setAllowEntryReordering(false);
-
- // WHEN the list is originally built with reordering disabled (and section changes allowed)
- addNotif(0, PACKAGE_1).setRank(2);
- addNotif(1, PACKAGE_1).setRank(3);
- addGroupSummary(2, PACKAGE_1, "group").setRank(4);
- addGroupChild(3, PACKAGE_1, "group").setRank(5);
- addGroupChild(4, PACKAGE_1, "group").setRank(6);
- dispatchBuild();
-
- verifyBuiltList(
- notif(0),
- notif(1),
- group(
- summary(2),
- child(3),
- child(4)
- )
- );
-
- // WHEN the notification summary rank increases and children removed
- setNewRank(notif(2).entry, 1);
- mEntrySet.remove(4);
- mEntrySet.remove(3);
- dispatchBuild();
-
- // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
- // despite visual stability being active
- verifyBuiltList(
- notif(2),
- notif(0),
- notif(1)
- );
- }
-
- @Test
public void groupRevertingToSummaryRetainsStablePosition() {
- when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
-
// GIVEN a notification group is on screen
mStabilityManager.setAllowEntryReordering(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
index 8275c0c..9b3626b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
@@ -127,6 +127,6 @@
NotificationManager.IMPORTANCE_DEFAULT,
null, null,
null, null, null, true, 0, false, -1, false, null, null, false, false,
- false, null, 0, false)
+ false, null, 0, false, 0)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index be6b1dc..2686238 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -321,7 +321,7 @@
val testDispatcher = UnconfinedTestDispatcher()
val testScope = TestScope(testDispatcher)
val fakeSettings = FakeSettings().apply {
- putBool(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, true)
+ putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
}
val seenNotificationsProvider = SeenNotificationsProviderImpl()
val keyguardCoordinator =
@@ -372,14 +372,14 @@
var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
get() =
- fakeSettings.getBoolForUser(
+ fakeSettings.getIntForUser(
Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
UserHandle.USER_CURRENT,
- )
+ ) == 1
set(value) {
- fakeSettings.putBoolForUser(
+ fakeSettings.putIntForUser(
Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
- value,
+ if (value) 1 else 2,
UserHandle.USER_CURRENT,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 601771d..03af527 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -41,6 +41,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.PendingIntent;
@@ -59,6 +60,7 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -107,6 +109,8 @@
UiEventLoggerFake mUiEventLoggerFake;
@Mock
PendingIntent mPendingIntent;
+ @Mock
+ UserTracker mUserTracker;
private NotificationInterruptStateProviderImpl mNotifInterruptionStateProvider;
@@ -114,6 +118,7 @@
public void setup() {
MockitoAnnotations.initMocks(this);
when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
mUiEventLoggerFake = new UiEventLoggerFake();
@@ -131,7 +136,8 @@
mMockHandler,
mFlags,
mKeyguardNotificationVisibilityProvider,
- mUiEventLoggerFake);
+ mUiEventLoggerFake,
+ mUserTracker);
mNotifInterruptionStateProvider.mUseHeadsUp = true;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 9d531a1..9e23d54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -44,16 +44,23 @@
import android.app.Notification;
import android.app.NotificationChannel;
import android.graphics.Color;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.util.DisplayMetrics;
import android.view.View;
+import android.widget.ImageView;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
+import com.android.internal.widget.CachingIconView;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
@@ -61,6 +68,7 @@
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
import org.junit.Assert;
@@ -72,20 +80,16 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.Arrays;
import java.util.List;
+import java.util.function.Consumer;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
public class ExpandableNotificationRowTest extends SysuiTestCase {
- private ExpandableNotificationRow mGroupRow;
- private ExpandableNotificationRow mNotifRow;
- private ExpandableNotificationRow mPublicRow;
-
private NotificationTestHelper mNotificationTestHelper;
- boolean mHeadsUpAnimatingAway = false;
-
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Before
@@ -96,109 +100,108 @@
mDependency,
TestableLooper.get(this));
mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
- // create a standard private notification row
- Notification normalNotif = mNotificationTestHelper.createNotification();
- normalNotif.publicVersion = null;
- mNotifRow = mNotificationTestHelper.createRow(normalNotif);
+
+ FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
+ fakeFeatureFlags.set(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE, true);
+ mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags);
+ }
+
+ @Test
+ public void testUpdateBackgroundColors_isRecursive() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+ group.setTintColor(Color.RED);
+ group.getChildNotificationAt(0).setTintColor(Color.GREEN);
+ group.getChildNotificationAt(1).setTintColor(Color.BLUE);
+
+ assertThat(group.getCurrentBackgroundTint()).isEqualTo(Color.RED);
+ assertThat(group.getChildNotificationAt(0).getCurrentBackgroundTint())
+ .isEqualTo(Color.GREEN);
+ assertThat(group.getChildNotificationAt(1).getCurrentBackgroundTint())
+ .isEqualTo(Color.BLUE);
+
+ group.updateBackgroundColors();
+
+ int resetTint = group.getCurrentBackgroundTint();
+ assertThat(resetTint).isNotEqualTo(Color.RED);
+ assertThat(group.getChildNotificationAt(0).getCurrentBackgroundTint())
+ .isEqualTo(resetTint);
+ assertThat(group.getChildNotificationAt(1).getCurrentBackgroundTint())
+ .isEqualTo(resetTint);
+ }
+
+ @Test
+ public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
+ // GIVEN a sensitive notification row that's currently redacted
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ measureAndLayout(row);
+ row.setHideSensitiveForIntrinsicHeight(true);
+ row.setSensitive(true, true);
+ assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout());
+ assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
+
+ // GIVEN that the row has a height change listener
+ OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
+ row.setOnHeightChangedListener(listener);
+
+ // WHEN the row is set to no longer be sensitive
+ row.setSensitive(false, true);
+
+ // VERIFY that the height change listener is invoked
+ assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
+ assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
+ verify(listener).onHeightChanged(eq(row), eq(false));
+ }
+
+ @Test
+ public void testSetSensitiveOnGroupRowNotifiesOfHeightChange() throws Exception {
+ // GIVEN a sensitive group row that's currently redacted
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+ measureAndLayout(group);
+ group.setHideSensitiveForIntrinsicHeight(true);
+ group.setSensitive(true, true);
+ assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPublicLayout());
+ assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
+
+ // GIVEN that the row has a height change listener
+ OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
+ group.setOnHeightChangedListener(listener);
+
+ // WHEN the row is set to no longer be sensitive
+ group.setSensitive(false, true);
+
+ // VERIFY that the height change listener is invoked
+ assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPrivateLayout());
+ assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
+ verify(listener).onHeightChanged(eq(group), eq(false));
+ }
+
+ @Test
+ public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange() throws Exception {
// create a notification row whose public version is identical
Notification publicNotif = mNotificationTestHelper.createNotification();
publicNotif.publicVersion = mNotificationTestHelper.createNotification();
- mPublicRow = mNotificationTestHelper.createRow(publicNotif);
- // create a group row
- mGroupRow = mNotificationTestHelper.createGroup();
- mGroupRow.setHeadsUpAnimatingAwayListener(
- animatingAway -> mHeadsUpAnimatingAway = animatingAway);
+ ExpandableNotificationRow publicRow = mNotificationTestHelper.createRow(publicNotif);
- }
-
- @Test
- public void testUpdateBackgroundColors_isRecursive() {
- mGroupRow.setTintColor(Color.RED);
- mGroupRow.getChildNotificationAt(0).setTintColor(Color.GREEN);
- mGroupRow.getChildNotificationAt(1).setTintColor(Color.BLUE);
-
- assertThat(mGroupRow.getCurrentBackgroundTint()).isEqualTo(Color.RED);
- assertThat(mGroupRow.getChildNotificationAt(0).getCurrentBackgroundTint())
- .isEqualTo(Color.GREEN);
- assertThat(mGroupRow.getChildNotificationAt(1).getCurrentBackgroundTint())
- .isEqualTo(Color.BLUE);
-
- mGroupRow.updateBackgroundColors();
-
- int resetTint = mGroupRow.getCurrentBackgroundTint();
- assertThat(resetTint).isNotEqualTo(Color.RED);
- assertThat(mGroupRow.getChildNotificationAt(0).getCurrentBackgroundTint())
- .isEqualTo(resetTint);
- assertThat(mGroupRow.getChildNotificationAt(1).getCurrentBackgroundTint())
- .isEqualTo(resetTint);
- }
-
- @Test
- public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws InterruptedException {
- // GIVEN a sensitive notification row that's currently redacted
- measureAndLayout(mNotifRow);
- mNotifRow.setHideSensitiveForIntrinsicHeight(true);
- mNotifRow.setSensitive(true, true);
- assertThat(mNotifRow.getShowingLayout()).isSameInstanceAs(mNotifRow.getPublicLayout());
- assertThat(mNotifRow.getIntrinsicHeight()).isGreaterThan(0);
-
- // GIVEN that the row has a height change listener
- OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
- mNotifRow.setOnHeightChangedListener(listener);
-
- // WHEN the row is set to no longer be sensitive
- mNotifRow.setSensitive(false, true);
-
- // VERIFY that the height change listener is invoked
- assertThat(mNotifRow.getShowingLayout()).isSameInstanceAs(mNotifRow.getPrivateLayout());
- assertThat(mNotifRow.getIntrinsicHeight()).isGreaterThan(0);
- verify(listener).onHeightChanged(eq(mNotifRow), eq(false));
- }
-
- @Test
- public void testSetSensitiveOnGroupRowNotifiesOfHeightChange() {
- // GIVEN a sensitive group row that's currently redacted
- measureAndLayout(mGroupRow);
- mGroupRow.setHideSensitiveForIntrinsicHeight(true);
- mGroupRow.setSensitive(true, true);
- assertThat(mGroupRow.getShowingLayout()).isSameInstanceAs(mGroupRow.getPublicLayout());
- assertThat(mGroupRow.getIntrinsicHeight()).isGreaterThan(0);
-
- // GIVEN that the row has a height change listener
- OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
- mGroupRow.setOnHeightChangedListener(listener);
-
- // WHEN the row is set to no longer be sensitive
- mGroupRow.setSensitive(false, true);
-
- // VERIFY that the height change listener is invoked
- assertThat(mGroupRow.getShowingLayout()).isSameInstanceAs(mGroupRow.getPrivateLayout());
- assertThat(mGroupRow.getIntrinsicHeight()).isGreaterThan(0);
- verify(listener).onHeightChanged(eq(mGroupRow), eq(false));
- }
-
- @Test
- public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange() {
// GIVEN a sensitive public row that's currently redacted
- measureAndLayout(mPublicRow);
- mPublicRow.setHideSensitiveForIntrinsicHeight(true);
- mPublicRow.setSensitive(true, true);
- assertThat(mPublicRow.getShowingLayout()).isSameInstanceAs(mPublicRow.getPublicLayout());
- assertThat(mPublicRow.getIntrinsicHeight()).isGreaterThan(0);
+ measureAndLayout(publicRow);
+ publicRow.setHideSensitiveForIntrinsicHeight(true);
+ publicRow.setSensitive(true, true);
+ assertThat(publicRow.getShowingLayout()).isSameInstanceAs(publicRow.getPublicLayout());
+ assertThat(publicRow.getIntrinsicHeight()).isGreaterThan(0);
// GIVEN that the row has a height change listener
OnHeightChangedListener listener = mock(OnHeightChangedListener.class);
- mPublicRow.setOnHeightChangedListener(listener);
+ publicRow.setOnHeightChangedListener(listener);
// WHEN the row is set to no longer be sensitive
- mPublicRow.setSensitive(false, true);
+ publicRow.setSensitive(false, true);
// VERIFY that the height change listener is not invoked, because the height didn't change
- assertThat(mPublicRow.getShowingLayout()).isSameInstanceAs(mPublicRow.getPrivateLayout());
- assertThat(mPublicRow.getIntrinsicHeight()).isGreaterThan(0);
- assertThat(mPublicRow.getPrivateLayout().getMinHeight())
- .isEqualTo(mPublicRow.getPublicLayout().getMinHeight());
- verify(listener, never()).onHeightChanged(eq(mPublicRow), eq(false));
+ assertThat(publicRow.getShowingLayout()).isSameInstanceAs(publicRow.getPrivateLayout());
+ assertThat(publicRow.getIntrinsicHeight()).isGreaterThan(0);
+ assertThat(publicRow.getPrivateLayout().getMinHeight())
+ .isEqualTo(publicRow.getPublicLayout().getMinHeight());
+ verify(listener, never()).onHeightChanged(eq(publicRow), eq(false));
}
private void measureAndLayout(ExpandableNotificationRow row) {
@@ -215,36 +218,43 @@
}
@Test
- public void testGroupSummaryNotShowingIconWhenPublic() {
- mGroupRow.setSensitive(true, true);
- mGroupRow.setHideSensitiveForIntrinsicHeight(true);
- assertTrue(mGroupRow.isSummaryWithChildren());
- assertFalse(mGroupRow.isShowingIcon());
+ public void testGroupSummaryNotShowingIconWhenPublic() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+ group.setSensitive(true, true);
+ group.setHideSensitiveForIntrinsicHeight(true);
+ assertTrue(group.isSummaryWithChildren());
+ assertFalse(group.isShowingIcon());
}
@Test
- public void testNotificationHeaderVisibleWhenAnimating() {
- mGroupRow.setSensitive(true, true);
- mGroupRow.setHideSensitive(true, false, 0, 0);
- mGroupRow.setHideSensitive(false, true, 0, 0);
- assertEquals(View.VISIBLE, mGroupRow.getChildrenContainer().getVisibleWrapper()
+ public void testNotificationHeaderVisibleWhenAnimating() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+ group.setSensitive(true, true);
+ group.setHideSensitive(true, false, 0, 0);
+ group.setHideSensitive(false, true, 0, 0);
+ assertEquals(View.VISIBLE, group.getChildrenContainer().getVisibleWrapper()
.getNotificationHeader().getVisibility());
}
@Test
- public void testUserLockedResetEvenWhenNoChildren() {
- mGroupRow.setUserLocked(true);
- mGroupRow.setUserLocked(false);
+ public void testUserLockedResetEvenWhenNoChildren() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+ group.setUserLocked(true);
+ group.setUserLocked(false);
assertFalse("The childrencontainer should not be userlocked but is, the state "
- + "seems out of sync.", mGroupRow.getChildrenContainer().isUserLocked());
+ + "seems out of sync.", group.getChildrenContainer().isUserLocked());
}
@Test
- public void testReinflatedOnDensityChange() {
+ public void testReinflatedOnDensityChange() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
- mNotifRow.setChildrenContainer(mockContainer);
+ row.setChildrenContainer(mockContainer);
- mNotifRow.onDensityOrFontScaleChanged();
+ row.onDensityOrFontScaleChanged();
verify(mockContainer).reInflateViews(any(), any());
}
@@ -287,64 +297,73 @@
@Test
public void testAboveShelfChangedListenerCalledWhenGoingBelow() throws Exception {
ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setHeadsUp(true);
AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class);
row.setAboveShelfChangedListener(listener);
+ Mockito.reset(listener);
+ row.setHeadsUp(true);
row.setAboveShelf(false);
verify(listener).onAboveShelfStateChanged(false);
}
@Test
public void testClickSound() throws Exception {
- assertTrue("Should play sounds by default.", mGroupRow.isSoundEffectsEnabled());
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+ assertTrue("Should play sounds by default.", group.isSoundEffectsEnabled());
StatusBarStateController mock = mNotificationTestHelper.getStatusBarStateController();
when(mock.isDozing()).thenReturn(true);
- mGroupRow.setSecureStateProvider(()-> false);
+ group.setSecureStateProvider(()-> false);
assertFalse("Shouldn't play sounds when dark and trusted.",
- mGroupRow.isSoundEffectsEnabled());
- mGroupRow.setSecureStateProvider(()-> true);
+ group.isSoundEffectsEnabled());
+ group.setSecureStateProvider(()-> true);
assertTrue("Should always play sounds when not trusted.",
- mGroupRow.isSoundEffectsEnabled());
+ group.isSoundEffectsEnabled());
}
@Test
- public void testSetDismissed_longPressListenerRemoved() {
+ public void testSetDismissed_longPressListenerRemoved() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
ExpandableNotificationRow.LongPressListener listener =
mock(ExpandableNotificationRow.LongPressListener.class);
- mGroupRow.setLongPressListener(listener);
- mGroupRow.doLongClickCallback(0,0);
- verify(listener, times(1)).onLongPress(eq(mGroupRow), eq(0), eq(0),
+ group.setLongPressListener(listener);
+ group.doLongClickCallback(0, 0);
+ verify(listener, times(1)).onLongPress(eq(group), eq(0), eq(0),
any(NotificationMenuRowPlugin.MenuItem.class));
reset(listener);
- mGroupRow.dismiss(true);
- mGroupRow.doLongClickCallback(0,0);
- verify(listener, times(0)).onLongPress(eq(mGroupRow), eq(0), eq(0),
+ group.dismiss(true);
+ group.doLongClickCallback(0, 0);
+ verify(listener, times(0)).onLongPress(eq(group), eq(0), eq(0),
any(NotificationMenuRowPlugin.MenuItem.class));
}
@Test
- public void testFeedback_noHeader() {
+ public void testFeedback_noHeader() throws Exception {
+ ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
+
// public notification is custom layout - no header
- mGroupRow.setSensitive(true, true);
- mGroupRow.setOnFeedbackClickListener(null);
- mGroupRow.setFeedbackIcon(null);
+ groupRow.setSensitive(true, true);
+ groupRow.setOnFeedbackClickListener(null);
+ groupRow.setFeedbackIcon(null);
}
@Test
- public void testFeedback_header() {
+ public void testFeedback_header() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
NotificationContentView publicLayout = mock(NotificationContentView.class);
- mGroupRow.setPublicLayout(publicLayout);
+ group.setPublicLayout(publicLayout);
NotificationContentView privateLayout = mock(NotificationContentView.class);
- mGroupRow.setPrivateLayout(privateLayout);
+ group.setPrivateLayout(privateLayout);
NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
when(mockContainer.getNotificationChildCount()).thenReturn(1);
- mGroupRow.setChildrenContainer(mockContainer);
+ group.setChildrenContainer(mockContainer);
final boolean show = true;
final FeedbackIcon icon = new FeedbackIcon(
R.drawable.ic_feedback_alerted, R.string.notification_feedback_indicator_alerted);
- mGroupRow.setFeedbackIcon(icon);
+ group.setFeedbackIcon(icon);
verify(mockContainer, times(1)).setFeedbackIcon(icon);
verify(privateLayout, times(1)).setFeedbackIcon(icon);
@@ -352,43 +371,60 @@
}
@Test
- public void testFeedbackOnClick() {
+ public void testFeedbackOnClick() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
ExpandableNotificationRow.CoordinateOnClickListener l = mock(
ExpandableNotificationRow.CoordinateOnClickListener.class);
View view = mock(View.class);
- mGroupRow.setOnFeedbackClickListener(l);
+ group.setOnFeedbackClickListener(l);
- mGroupRow.getFeedbackOnClickListener().onClick(view);
+ group.getFeedbackOnClickListener().onClick(view);
verify(l, times(1)).onClick(any(), anyInt(), anyInt(), any());
}
@Test
- public void testHeadsUpAnimatingAwayListener() {
- mGroupRow.setHeadsUpAnimatingAway(true);
- Assert.assertEquals(true, mHeadsUpAnimatingAway);
- mGroupRow.setHeadsUpAnimatingAway(false);
- Assert.assertEquals(false, mHeadsUpAnimatingAway);
+ public void testHeadsUpAnimatingAwayListener() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+ Consumer<Boolean> headsUpListener = mock(Consumer.class);
+ AboveShelfChangedListener aboveShelfChangedListener = mock(AboveShelfChangedListener.class);
+ group.setHeadsUpAnimatingAwayListener(headsUpListener);
+ group.setAboveShelfChangedListener(aboveShelfChangedListener);
+
+ group.setHeadsUpAnimatingAway(true);
+ verify(headsUpListener).accept(true);
+ verify(aboveShelfChangedListener).onAboveShelfStateChanged(true);
+
+ group.setHeadsUpAnimatingAway(false);
+ verify(headsUpListener).accept(false);
+ verify(aboveShelfChangedListener).onAboveShelfStateChanged(false);
}
@Test
- public void testIsBlockingHelperShowing_isCorrectlyUpdated() {
- mGroupRow.setBlockingHelperShowing(true);
- assertTrue(mGroupRow.isBlockingHelperShowing());
+ public void testIsBlockingHelperShowing_isCorrectlyUpdated() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
- mGroupRow.setBlockingHelperShowing(false);
- assertFalse(mGroupRow.isBlockingHelperShowing());
+ group.setBlockingHelperShowing(true);
+ assertTrue(group.isBlockingHelperShowing());
+
+ group.setBlockingHelperShowing(false);
+ assertFalse(group.isBlockingHelperShowing());
}
@Test
- public void testGetNumUniqueChildren_defaultChannel() {
- assertEquals(1, mGroupRow.getNumUniqueChannels());
+ public void testGetNumUniqueChildren_defaultChannel() throws Exception {
+ ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
+
+ assertEquals(1, groupRow.getNumUniqueChannels());
}
@Test
- public void testGetNumUniqueChildren_multiChannel() {
+ public void testGetNumUniqueChildren_multiChannel() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
List<ExpandableNotificationRow> childRows =
- mGroupRow.getChildrenContainer().getAttachedChildren();
+ group.getChildrenContainer().getAttachedChildren();
// Give each child a unique channel id/name.
int i = 0;
for (ExpandableNotificationRow childRow : childRows) {
@@ -400,25 +436,29 @@
i++;
}
- assertEquals(3, mGroupRow.getNumUniqueChannels());
+ assertEquals(3, group.getNumUniqueChannels());
}
@Test
public void testIconScrollXAfterTranslationAndReset() throws Exception {
- mGroupRow.setDismissUsingRowTranslationX(false);
- mGroupRow.setTranslation(50);
- assertEquals(50, -mGroupRow.getEntry().getIcons().getShelfIcon().getScrollX());
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
- mGroupRow.resetTranslation();
- assertEquals(0, mGroupRow.getEntry().getIcons().getShelfIcon().getScrollX());
+ group.setDismissUsingRowTranslationX(false);
+ group.setTranslation(50);
+ assertEquals(50, -group.getEntry().getIcons().getShelfIcon().getScrollX());
+
+ group.resetTranslation();
+ assertEquals(0, group.getEntry().getIcons().getShelfIcon().getScrollX());
}
@Test
- public void testIsExpanded_userExpanded() {
- mGroupRow.setExpandable(true);
- Assert.assertFalse(mGroupRow.isExpanded());
- mGroupRow.setUserExpanded(true);
- Assert.assertTrue(mGroupRow.isExpanded());
+ public void testIsExpanded_userExpanded() throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+
+ group.setExpandable(true);
+ Assert.assertFalse(group.isExpanded());
+ group.setUserExpanded(true);
+ Assert.assertTrue(group.isExpanded());
}
@Test
@@ -537,26 +577,155 @@
}
@Test
- public void applyRoundnessAndInv_should_be_immediately_applied_on_childrenContainer_legacy() {
- mGroupRow.useRoundnessSourceTypes(false);
- Assert.assertEquals(0f, mGroupRow.getBottomRoundness(), 0.001f);
- Assert.assertEquals(0f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+ public void applyRoundnessAndInv_should_be_immediately_applied_on_childrenContainer_legacy()
+ throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+ group.useRoundnessSourceTypes(false);
+ Assert.assertEquals(0f, group.getBottomRoundness(), 0.001f);
+ Assert.assertEquals(0f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
- mGroupRow.requestBottomRoundness(1f, SourceType.from(""), false);
+ group.requestBottomRoundness(1f, SourceType.from(""), false);
- Assert.assertEquals(1f, mGroupRow.getBottomRoundness(), 0.001f);
- Assert.assertEquals(1f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+ Assert.assertEquals(1f, group.getBottomRoundness(), 0.001f);
+ Assert.assertEquals(1f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
}
@Test
- public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_childrenContainer() {
- mGroupRow.useRoundnessSourceTypes(true);
- Assert.assertEquals(0f, mGroupRow.getBottomRoundness(), 0.001f);
- Assert.assertEquals(0f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+ public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_childrenContainer()
+ throws Exception {
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+ group.useRoundnessSourceTypes(true);
+ Assert.assertEquals(0f, group.getBottomRoundness(), 0.001f);
+ Assert.assertEquals(0f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
- mGroupRow.requestBottomRoundness(1f, SourceType.from(""), false);
+ group.requestBottomRoundness(1f, SourceType.from(""), false);
- Assert.assertEquals(1f, mGroupRow.getBottomRoundness(), 0.001f);
- Assert.assertEquals(1f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
+ Assert.assertEquals(1f, group.getBottomRoundness(), 0.001f);
+ Assert.assertEquals(1f, group.getChildrenContainer().getBottomRoundness(), 0.001f);
+ }
+
+ @Test
+ public void testSetContentAnimationRunning_Run() throws Exception {
+ // Create views for the notification row.
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ NotificationContentView publicLayout = mock(NotificationContentView.class);
+ row.setPublicLayout(publicLayout);
+ NotificationContentView privateLayout = mock(NotificationContentView.class);
+ row.setPrivateLayout(privateLayout);
+
+ row.setAnimationRunning(true);
+ verify(publicLayout, times(1)).setContentAnimationRunning(true);
+ verify(privateLayout, times(1)).setContentAnimationRunning(true);
+ }
+
+ @Test
+ public void testSetContentAnimationRunning_Stop() throws Exception {
+ // Create views for the notification row.
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ NotificationContentView publicLayout = mock(NotificationContentView.class);
+ row.setPublicLayout(publicLayout);
+ NotificationContentView privateLayout = mock(NotificationContentView.class);
+ row.setPrivateLayout(privateLayout);
+
+ row.setAnimationRunning(false);
+ verify(publicLayout, times(1)).setContentAnimationRunning(false);
+ verify(privateLayout, times(1)).setContentAnimationRunning(false);
+ }
+
+ @Test
+ public void testSetContentAnimationRunningInGroupChild_Run() throws Exception {
+ // Creates parent views on groupRow.
+ ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
+ NotificationContentView publicParentLayout = mock(NotificationContentView.class);
+ groupRow.setPublicLayout(publicParentLayout);
+ NotificationContentView privateParentLayout = mock(NotificationContentView.class);
+ groupRow.setPrivateLayout(privateParentLayout);
+
+ // Create child views on row.
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ NotificationContentView publicChildLayout = mock(NotificationContentView.class);
+ row.setPublicLayout(publicChildLayout);
+ NotificationContentView privateChildLayout = mock(NotificationContentView.class);
+ row.setPrivateLayout(privateChildLayout);
+ when(row.isGroupExpanded()).thenReturn(true);
+ setMockChildrenContainer(groupRow, row);
+
+ groupRow.setAnimationRunning(true);
+ verify(publicParentLayout, times(1)).setContentAnimationRunning(true);
+ verify(privateParentLayout, times(1)).setContentAnimationRunning(true);
+ // The child layouts should be started too.
+ verify(publicChildLayout, times(1)).setContentAnimationRunning(true);
+ verify(privateChildLayout, times(1)).setContentAnimationRunning(true);
+ }
+
+
+ @Test
+ public void testSetIconAnimationRunningGroup_Run() throws Exception {
+ // Create views for a group row.
+ ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
+ ExpandableNotificationRow child = mNotificationTestHelper.createRow();
+ NotificationContentView publicParentLayout = mock(NotificationContentView.class);
+ group.setPublicLayout(publicParentLayout);
+ NotificationContentView privateParentLayout = mock(NotificationContentView.class);
+ group.setPrivateLayout(privateParentLayout);
+ when(group.isGroupExpanded()).thenReturn(true);
+
+ // Add the child to the group.
+ NotificationContentView publicChildLayout = mock(NotificationContentView.class);
+ child.setPublicLayout(publicChildLayout);
+ NotificationContentView privateChildLayout = mock(NotificationContentView.class);
+ child.setPrivateLayout(privateChildLayout);
+ when(child.isGroupExpanded()).thenReturn(true);
+
+ NotificationChildrenContainer mockContainer =
+ setMockChildrenContainer(group, child);
+
+ // Mock the children view wrappers, and give them each an icon.
+ NotificationViewWrapper mockViewWrapper = mock(NotificationViewWrapper.class);
+ when(mockContainer.getNotificationViewWrapper()).thenReturn(mockViewWrapper);
+ CachingIconView mockIcon = mock(CachingIconView.class);
+ when(mockViewWrapper.getIcon()).thenReturn(mockIcon);
+
+ NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class);
+ when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper);
+ CachingIconView mockLowPriorityIcon = mock(CachingIconView.class);
+ when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon);
+
+ // Give the icon image views drawables, so we can make sure they animate.
+ // We use both AnimationDrawables and AnimatedVectorDrawables to ensure both work.
+ AnimationDrawable drawable = mock(AnimationDrawable.class);
+ AnimatedVectorDrawable vectorDrawable = mock(AnimatedVectorDrawable.class);
+ setDrawableIconsInImageView(mockIcon, drawable, vectorDrawable);
+
+ AnimationDrawable lowPriDrawable = mock(AnimationDrawable.class);
+ AnimatedVectorDrawable lowPriVectorDrawable = mock(AnimatedVectorDrawable.class);
+ setDrawableIconsInImageView(mockLowPriorityIcon, lowPriDrawable, lowPriVectorDrawable);
+
+ group.setAnimationRunning(true);
+ verify(drawable, times(1)).start();
+ verify(vectorDrawable, times(1)).start();
+ verify(lowPriDrawable, times(1)).start();
+ verify(lowPriVectorDrawable, times(1)).start();
+ }
+
+ private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable,
+ Drawable rightIconDrawable) {
+ ImageView iconView = mock(ImageView.class);
+ when(icon.findViewById(com.android.internal.R.id.icon)).thenReturn(iconView);
+ when(iconView.getDrawable()).thenReturn(iconDrawable);
+
+ ImageView rightIconView = mock(ImageView.class);
+ when(icon.findViewById(com.android.internal.R.id.right_icon)).thenReturn(rightIconView);
+ when(rightIconView.getDrawable()).thenReturn(rightIconDrawable);
+ }
+
+ private NotificationChildrenContainer setMockChildrenContainer(
+ ExpandableNotificationRow parentRow, ExpandableNotificationRow childRow) {
+ List<ExpandableNotificationRow> rowList = Arrays.asList(childRow);
+ NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
+ when(mockContainer.getNotificationChildCount()).thenReturn(1);
+ when(mockContainer.getAttachedChildren()).thenReturn(rowList);
+ parentRow.setChildrenContainer(mockContainer);
+ return mockContainer;
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
index 1f92b0a..819a75b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.row;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
@@ -98,5 +100,16 @@
mView.setSecondaryVisible(true /* visible */, true /* animate */);
}
+
+ @Test
+ public void testSetFooterLabelTextAndIcon() {
+ mView.setFooterLabelTextAndIcon(
+ R.string.unlock_to_see_notif_text,
+ R.drawable.ic_friction_lock_closed);
+ assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.GONE);
+ assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.GONE);
+ assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility())
+ .isEqualTo(View.VISIBLE);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 562b4df..7b2051d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -34,9 +34,12 @@
import com.android.systemui.statusbar.notification.FeedbackIcon
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -44,6 +47,7 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
@@ -305,6 +309,86 @@
assertEquals(0, getMarginBottom(actionListMarginTarget))
}
+ @Test
+ fun onSetAnimationRunning() {
+ // Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set
+ val mockContracted = mock<NotificationViewWrapper>()
+ val mockExpanded = mock<NotificationViewWrapper>()
+ val mockHeadsUp = mock<NotificationViewWrapper>()
+
+ view.setContractedWrapper(mockContracted)
+ view.setExpandedWrapper(mockExpanded)
+ view.setHeadsUpWrapper(mockHeadsUp)
+
+ // When: we set content animation running.
+ assertTrue(view.setContentAnimationRunning(true))
+
+ // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
+ // called on them.
+ verify(mockContracted, times(1)).setAnimationsRunning(true)
+ verify(mockExpanded, times(1)).setAnimationsRunning(true)
+ verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+
+ // When: we set content animation running true _again_.
+ assertFalse(view.setContentAnimationRunning(true))
+
+ // Then: the children should not have setAnimationRunning called on them again.
+ // Verify counts number of calls so far on the object, so these still register as 1.
+ verify(mockContracted, times(1)).setAnimationsRunning(true)
+ verify(mockExpanded, times(1)).setAnimationsRunning(true)
+ verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+ }
+
+ @Test
+ fun onSetAnimationStopped() {
+ // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
+ val mockContracted = mock<NotificationViewWrapper>()
+ val mockExpanded = mock<NotificationViewWrapper>()
+ val mockHeadsUp = mock<NotificationViewWrapper>()
+
+ view.setContractedWrapper(mockContracted)
+ view.setExpandedWrapper(mockExpanded)
+ view.setHeadsUpWrapper(mockHeadsUp)
+
+ // When: we set content animation running.
+ assertTrue(view.setContentAnimationRunning(true))
+
+ // Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
+ // called on them.
+ verify(mockContracted).setAnimationsRunning(true)
+ verify(mockExpanded).setAnimationsRunning(true)
+ verify(mockHeadsUp).setAnimationsRunning(true)
+
+ // When: we set content animation running false, the state changes, so the function
+ // returns true.
+ assertTrue(view.setContentAnimationRunning(false))
+
+ // Then: the children have their animations stopped.
+ verify(mockContracted).setAnimationsRunning(false)
+ verify(mockExpanded).setAnimationsRunning(false)
+ verify(mockHeadsUp).setAnimationsRunning(false)
+ }
+
+ @Test
+ fun onSetAnimationInitStopped() {
+ // Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
+ val mockContracted = mock<NotificationViewWrapper>()
+ val mockExpanded = mock<NotificationViewWrapper>()
+ val mockHeadsUp = mock<NotificationViewWrapper>()
+
+ view.setContractedWrapper(mockContracted)
+ view.setExpandedWrapper(mockExpanded)
+ view.setHeadsUpWrapper(mockHeadsUp)
+
+ // When: we try to stop the animations before they've been started.
+ assertFalse(view.setContentAnimationRunning(false))
+
+ // Then: the children should not have setAnimationRunning called on them again.
+ verify(mockContracted, never()).setAnimationsRunning(false)
+ verify(mockExpanded, never()).setAnimationsRunning(false)
+ verify(mockHeadsUp, never()).setAnimationsRunning(false)
+ }
+
private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
mock<ExpandableNotificationRow>().apply {
whenever(this.entry).thenReturn(notificationEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index b6a1bb3..d7ac6b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -66,9 +66,9 @@
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.testing.UiEventLoggerFake;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -133,18 +133,13 @@
@Mock private ShadeController mShadeController;
@Mock private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager;
@Mock private AssistantFeedbackController mAssistantFeedbackController;
+ @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
+ @Mock private StatusBarStateController mStatusBarStateController;
@Before
public void setUp() {
mTestableLooper = TestableLooper.get(this);
allowTestableLooperAsMainThread();
- mDependency.injectTestDependency(DeviceProvisionedController.class,
- mDeviceProvisionedController);
- mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
- mDependency.injectTestDependency(
- OnUserInteractionCallback.class,
- mOnUserInteractionCallback);
- mDependency.injectMockDependency(NotificationLockscreenUserManager.class);
mHandler = Handler.createAsync(mTestableLooper.getLooper());
mHelper = new NotificationTestHelper(mContext, mDependency, TestableLooper.get(this));
when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
@@ -155,7 +150,11 @@
mPeopleSpaceWidgetManager, mLauncherApps, mShortcutManager,
mChannelEditorDialogController, mContextTracker, mAssistantFeedbackController,
Optional.of(mBubblesManager), new UiEventLoggerFake(), mOnUserInteractionCallback,
- mShadeController);
+ mShadeController,
+ mNotificationLockscreenUserManager,
+ mStatusBarStateController,
+ mDeviceProvisionedController,
+ mMetricsLogger);
mGutsManager.setUpWithPresenter(mPresenter, mNotificationListContainer,
mOnSettingsClickListener);
mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
@@ -372,7 +371,8 @@
eq(false),
eq(false),
eq(true), /* wasShownHighPriority */
- eq(mAssistantFeedbackController));
+ eq(mAssistantFeedbackController),
+ any(MetricsLogger.class));
}
@Test
@@ -406,7 +406,8 @@
eq(true),
eq(false),
eq(false), /* wasShownHighPriority */
- eq(mAssistantFeedbackController));
+ eq(mAssistantFeedbackController),
+ any(MetricsLogger.class));
}
@Test
@@ -438,7 +439,8 @@
eq(false),
eq(false),
eq(false), /* wasShownHighPriority */
- eq(mAssistantFeedbackController));
+ eq(mAssistantFeedbackController),
+ any(MetricsLogger.class));
}
////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
index 80a81a5..8dd0488 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
@@ -130,7 +130,6 @@
mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
- mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
// Inflate the layout
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mNotificationInfo = (NotificationInfo) layoutInflater.inflate(R.layout.notification_info,
@@ -194,7 +193,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
assertTrue(textView.getText().toString().contains("App Name"));
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
@@ -220,7 +220,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon);
assertEquals(iconDrawable, iconView.getDrawable());
}
@@ -242,7 +243,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(GONE, nameView.getVisibility());
}
@@ -273,7 +275,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(VISIBLE, nameView.getVisibility());
assertTrue(nameView.getText().toString().contains("Proxied"));
@@ -296,7 +299,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
assertEquals(GONE, groupNameView.getVisibility());
}
@@ -324,7 +328,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
assertEquals(View.VISIBLE, groupNameView.getVisibility());
assertEquals("Test Group Name", groupNameView.getText());
@@ -347,7 +352,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(TEST_CHANNEL_NAME, textView.getText());
}
@@ -369,7 +375,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(GONE, textView.getVisibility());
}
@@ -395,7 +402,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(VISIBLE, textView.getVisibility());
}
@@ -417,7 +425,8 @@
true,
true,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(VISIBLE, textView.getVisibility());
}
@@ -443,7 +452,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
settingsButton.performClick();
@@ -468,7 +478,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
}
@@ -493,7 +504,8 @@
false,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
}
@@ -515,7 +527,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.bindNotification(
mMockPackageManager,
mMockINotificationManager,
@@ -531,7 +544,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertEquals(View.VISIBLE, settingsButton.getVisibility());
}
@@ -556,7 +570,8 @@
true,
true,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.info).performClick();
// Verify that listener was triggered.
@@ -582,7 +597,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView channelNameView =
mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(GONE, channelNameView.getVisibility());
@@ -606,7 +622,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(GONE, mNotificationInfo.findViewById(
R.id.interruptiveness_settings).getVisibility());
assertEquals(VISIBLE, mNotificationInfo.findViewById(
@@ -630,7 +647,8 @@
true,
true,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text);
assertEquals(View.VISIBLE, view.getVisibility());
assertEquals(mContext.getString(R.string.notification_unblockable_desc),
@@ -673,7 +691,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text);
assertEquals(View.VISIBLE, view.getVisibility());
assertEquals(mContext.getString(R.string.notification_unblockable_call_desc),
@@ -716,7 +735,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(GONE,
mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility());
assertEquals(VISIBLE,
@@ -743,7 +763,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
}
@@ -765,7 +786,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
}
@@ -789,7 +811,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected());
}
@@ -810,7 +833,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected());
}
@@ -831,7 +855,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected());
}
@@ -852,7 +877,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mTestableLooper.processAllMessages();
verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
anyString(), eq(TEST_UID), any());
@@ -875,7 +901,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(1, mUiEventLogger.numLogs());
assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
mUiEventLogger.eventId(0));
@@ -899,7 +926,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.alert).performClick();
mTestableLooper.processAllMessages();
@@ -926,7 +954,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.silence).performClick();
mTestableLooper.processAllMessages();
@@ -953,7 +982,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.automatic).performClick();
mTestableLooper.processAllMessages();
@@ -981,7 +1011,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.handleCloseControls(true, false);
mTestableLooper.processAllMessages();
@@ -1008,7 +1039,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.handleCloseControls(true, false);
mTestableLooper.processAllMessages();
@@ -1043,7 +1075,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.handleCloseControls(true, false);
@@ -1071,7 +1104,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1112,7 +1146,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1149,7 +1184,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.automatic).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1181,7 +1217,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1217,7 +1254,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(mContext.getString(R.string.inline_done_button),
((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1255,7 +1293,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.handleCloseControls(false, false);
@@ -1286,7 +1325,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(mContext.getString(R.string.inline_done_button),
((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1324,7 +1364,8 @@
true,
false,
true,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1353,7 +1394,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertEquals(mContext.getString(R.string.inline_done_button),
((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1384,7 +1426,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1419,7 +1462,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1452,7 +1496,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1485,7 +1530,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
mNotificationInfo.findViewById(R.id.alert).performClick();
@@ -1511,7 +1557,8 @@
true,
false,
false,
- mAssistantFeedbackController);
+ mAssistantFeedbackController,
+ mMetricsLogger);
assertFalse(mNotificationInfo.willBeRemoved());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 728e026..aca9c56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -40,7 +40,6 @@
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.graphics.drawable.Icon;
-import android.os.Handler;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.testing.TestableLooper;
@@ -49,14 +48,13 @@
import android.widget.RemoteViews;
import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
import com.android.systemui.TestableDependency;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -67,7 +65,6 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.icon.IconBuilder;
@@ -76,11 +73,8 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
-import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
@@ -120,16 +114,17 @@
private final GroupMembershipManager mGroupMembershipManager;
private final GroupExpansionManager mGroupExpansionManager;
private ExpandableNotificationRow mRow;
- private HeadsUpManagerPhone mHeadsUpManager;
+ private final HeadsUpManagerPhone mHeadsUpManager;
private final NotifBindPipeline mBindPipeline;
private final NotifCollectionListener mBindPipelineEntryListener;
private final RowContentBindStage mBindStage;
private final IconManager mIconManager;
- private StatusBarStateController mStatusBarStateController;
+ private final StatusBarStateController mStatusBarStateController;
private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
public final OnUserInteractionCallback mOnUserInteractionCallback;
public final Runnable mFutureDismissalRunnable;
private @InflationFlag int mDefaultInflationFlags;
+ private FeatureFlags mFeatureFlags;
public NotificationTestHelper(
Context context,
@@ -144,21 +139,7 @@
mStatusBarStateController = mock(StatusBarStateController.class);
mGroupMembershipManager = mock(GroupMembershipManager.class);
mGroupExpansionManager = mock(GroupExpansionManager.class);
- mHeadsUpManager = new HeadsUpManagerPhone(
- mContext,
- mock(HeadsUpManagerLogger.class),
- mStatusBarStateController,
- mock(KeyguardBypassController.class),
- mock(GroupMembershipManager.class),
- mock(VisualStabilityProvider.class),
- mock(ConfigurationControllerImpl.class),
- new Handler(mTestLooper.getLooper()),
- mock(AccessibilityManagerWrapper.class),
- mock(UiEventLogger.class),
- mock(ShadeExpansionStateManager.class)
- );
- mHeadsUpManager.mHandler.removeCallbacksAndMessages(null);
- mHeadsUpManager.mHandler = new Handler(mTestLooper.getLooper());
+ mHeadsUpManager = mock(HeadsUpManagerPhone.class);
mIconManager = new IconManager(
mock(CommonNotifCollection.class),
mock(LauncherApps.class),
@@ -193,12 +174,17 @@
mFutureDismissalRunnable = mock(Runnable.class);
when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
.thenReturn(mFutureDismissalRunnable);
+ mFeatureFlags = mock(FeatureFlags.class);
}
public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) {
mDefaultInflationFlags = defaultInflationFlags;
}
+ public void setFeatureFlags(FeatureFlags featureFlags) {
+ mFeatureFlags = featureFlags;
+ }
+
public ExpandableNotificationRowLogger getMockLogger() {
return mMockLogger;
}
@@ -561,7 +547,8 @@
mock(NotificationGutsManager.class),
mock(MetricsLogger.class),
mock(SmartReplyConstants.class),
- mock(SmartReplyController.class));
+ mock(SmartReplyController.class),
+ mFeatureFlags);
row.setAboveShelfChangedListener(aboveShelf -> { });
mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
index 509ba41..8f88501 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
@@ -16,11 +16,12 @@
package com.android.systemui.statusbar.notification.row.wrapper;
+import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
import android.app.Notification;
-import android.content.Context;
+import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.testing.AndroidTestingRunner;
@@ -28,11 +29,11 @@
import android.testing.TestableLooper.RunWithLooper;
import android.view.LayoutInflater;
import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
import androidx.test.filters.SmallTest;
+import com.android.internal.R;
+import com.android.internal.widget.BigPictureNotificationImageView;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -73,4 +74,38 @@
Notification.EXTRA_LARGE_ICON_BIG, new Bundle());
wrapper.onContentUpdated(mRow);
}
+
+ @Test
+ public void setAnimationsRunning_Run() {
+ BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture);
+ AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class);
+
+ assertNotNull(imageView);
+ imageView.setImageDrawable(mockDrawable);
+
+ NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext,
+ mView, mRow);
+ // Required to re-initialize the imageView to the imageView created above.
+ wrapper.onContentUpdated(mRow);
+
+ wrapper.setAnimationsRunning(true);
+ verify(mockDrawable).start();
+ }
+
+ @Test
+ public void setAnimationsRunning_Stop() {
+ BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture);
+ AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class);
+
+ assertNotNull(imageView);
+ imageView.setImageDrawable(mockDrawable);
+
+ NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext,
+ mView, mRow);
+ // Required to re-initialize the imageView to the imageView created above.
+ wrapper.onContentUpdated(mRow);
+
+ wrapper.setAnimationsRunning(false);
+ verify(mockDrawable).stop();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
new file mode 100644
index 0000000..3fa68bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.wrapper
+
+import android.graphics.drawable.AnimatedImageDrawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.widget.CachingIconView
+import com.android.internal.widget.ConversationLayout
+import com.android.internal.widget.MessagingGroup
+import com.android.internal.widget.MessagingImageMessage
+import com.android.internal.widget.MessagingLinearLayout
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() {
+
+ private lateinit var mRow: ExpandableNotificationRow
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ mRow = helper.createRow()
+ }
+
+ @Test
+ fun setAnimationsRunning_Run() {
+ // Creates a mocked out NotificationEntry of ConversationLayout type,
+ // with a mock imageMessage.drawable embedded in its MessagingImageMessages
+ // (both top level, and in a group).
+ val mockDrawable = mock<AnimatedImageDrawable>()
+ val mockDrawable2 = mock<AnimatedImageDrawable>()
+ val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2)
+
+ val wrapper: NotificationViewWrapper =
+ NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow)
+ wrapper.onContentUpdated(mRow)
+ wrapper.setAnimationsRunning(true)
+
+ // Verifies that each AnimatedImageDrawable is started animating.
+ verify(mockDrawable).start()
+ verify(mockDrawable2).start()
+ }
+
+ @Test
+ fun setAnimationsRunning_Stop() {
+ // Creates a mocked out NotificationEntry of ConversationLayout type,
+ // with a mock imageMessage.drawable embedded in its MessagingImageMessages
+ // (both top level, and in a group).
+ val mockDrawable = mock<AnimatedImageDrawable>()
+ val mockDrawable2 = mock<AnimatedImageDrawable>()
+ val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2)
+
+ val wrapper: NotificationViewWrapper =
+ NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow)
+ wrapper.onContentUpdated(mRow)
+ wrapper.setAnimationsRunning(false)
+
+ // Verifies that each AnimatedImageDrawable is started animating.
+ verify(mockDrawable).stop()
+ verify(mockDrawable2).stop()
+ }
+
+ private fun fakeConversationLayout(
+ mockDrawableGroupMessage: AnimatedImageDrawable,
+ mockDrawableImageMessage: AnimatedImageDrawable
+ ): View {
+ val mockMessagingImageMessage: MessagingImageMessage =
+ mock<MessagingImageMessage>().apply {
+ whenever(drawable).thenReturn(mockDrawableImageMessage)
+ }
+ val mockImageMessageContainer: MessagingLinearLayout =
+ mock<MessagingLinearLayout>().apply {
+ whenever(childCount).thenReturn(1)
+ whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage)
+ }
+
+ val mockMessagingImageMessageForGroup: MessagingImageMessage =
+ mock<MessagingImageMessage>().apply {
+ whenever(drawable).thenReturn(mockDrawableGroupMessage)
+ }
+ val mockMessageContainer: MessagingLinearLayout =
+ mock<MessagingLinearLayout>().apply {
+ whenever(childCount).thenReturn(1)
+ whenever(getChildAt(any())).thenReturn(mockMessagingImageMessageForGroup)
+ }
+ val mockGroup: MessagingGroup =
+ mock<MessagingGroup>().apply {
+ whenever(messageContainer).thenReturn(mockMessageContainer)
+ }
+ val mockView: View =
+ mock<ConversationLayout>().apply {
+ whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup)))
+ whenever(imageMessageContainer).thenReturn(mockImageMessageContainer)
+ whenever(messagingLinearLayout).thenReturn(mockMessageContainer)
+
+ // These must be mocked as they're required to be nonnull.
+ whenever(requireViewById<View>(R.id.conversation_icon_container)).thenReturn(mock())
+ whenever(requireViewById<CachingIconView>(R.id.conversation_icon))
+ .thenReturn(mock())
+ whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock())
+ whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock())
+ whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock())
+ whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock())
+ whenever(requireViewById<View>(R.id.conversation_icon_badge_ring))
+ .thenReturn(mock())
+ whenever(requireViewById<View>(R.id.app_name_text)).thenReturn(mock())
+ whenever(requireViewById<View>(R.id.conversation_text)).thenReturn(mock())
+ }
+ return mockView
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
new file mode 100644
index 0000000..c0444b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.wrapper
+
+import android.graphics.drawable.AnimatedImageDrawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.MessagingGroup
+import com.android.internal.widget.MessagingImageMessage
+import com.android.internal.widget.MessagingLayout
+import com.android.internal.widget.MessagingLinearLayout
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() {
+
+ private lateinit var mRow: ExpandableNotificationRow
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ mRow = helper.createRow()
+ }
+
+ @Test
+ fun setAnimationsRunning_Run() {
+ // Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type,
+ // with a mock imageMessage.drawable embedded in its MessagingImageMessage.
+ val mockDrawable = mock<AnimatedImageDrawable>()
+ val mockLayoutView: View = fakeMessagingLayout(mockDrawable)
+
+ val wrapper: NotificationViewWrapper =
+ NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow)
+ wrapper.setAnimationsRunning(true)
+
+ // Verifies that each AnimatedImageDrawable is started animating.
+ verify(mockDrawable).start()
+ }
+
+ @Test
+ fun setAnimationsRunning_Stop() {
+ // Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type,
+ // with a mock imageMessage.drawable embedded in its MessagingImageMessage.
+ val mockDrawable = mock<AnimatedImageDrawable>()
+ val mockLayoutView: View = fakeMessagingLayout(mockDrawable)
+
+ val wrapper: NotificationViewWrapper =
+ NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow)
+ wrapper.setAnimationsRunning(false)
+
+ // Verifies that each AnimatedImageDrawable is started animating.
+ verify(mockDrawable).stop()
+ }
+
+ private fun fakeMessagingLayout(mockDrawable: AnimatedImageDrawable): View {
+ val mockMessagingImageMessage: MessagingImageMessage =
+ mock<MessagingImageMessage>().apply { whenever(drawable).thenReturn(mockDrawable) }
+ val mockMessageContainer: MessagingLinearLayout =
+ mock<MessagingLinearLayout>().apply {
+ whenever(childCount).thenReturn(1)
+ whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage)
+ }
+ val mockGroup: MessagingGroup =
+ mock<MessagingGroup>().apply {
+ whenever(messageContainer).thenReturn(mockMessageContainer)
+ }
+ val mockView: View =
+ mock<MessagingLayout>().apply {
+ whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup)))
+ }
+ return mockView
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index ca99e24..e41929f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -29,6 +29,7 @@
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
import org.junit.Assert;
import org.junit.Before;
@@ -216,4 +217,29 @@
Assert.assertEquals(1f, mChildrenContainer.getBottomRoundness(), 0.001f);
Assert.assertEquals(1f, notificationRow.getBottomRoundness(), 0.001f);
}
+
+ @Test
+ public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_header() {
+ mChildrenContainer.useRoundnessSourceTypes(true);
+
+ NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper();
+ Assert.assertEquals(0f, header.getTopRoundness(), 0.001f);
+
+ mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false);
+
+ Assert.assertEquals(1f, header.getTopRoundness(), 0.001f);
+ }
+
+ @Test
+ public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() {
+ mChildrenContainer.useRoundnessSourceTypes(true);
+ mChildrenContainer.setIsLowPriority(true);
+
+ NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper();
+ Assert.assertEquals(0f, header.getTopRoundness(), 0.001f);
+
+ mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false);
+
+ Assert.assertEquals(1f, header.getTopRoundness(), 0.001f);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 645052f..9f6f082 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -21,6 +21,7 @@
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
@@ -65,6 +66,7 @@
import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl;
import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
+import com.android.systemui.statusbar.notification.collection.render.NotifStats;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -78,6 +80,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.Before;
import org.junit.Test;
@@ -135,10 +138,14 @@
@Mock private ShadeTransitionController mShadeTransitionController;
@Mock private FeatureFlags mFeatureFlags;
@Mock private NotificationTargetsHelper mNotificationTargetsHelper;
+ @Mock private SecureSettings mSecureSettings;
@Captor
private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
+ private final SeenNotificationsProviderImpl mSeenNotificationsProvider =
+ new SeenNotificationsProviderImpl();
+
private NotificationStackScrollLayoutController mController;
@Before
@@ -180,14 +187,15 @@
mUiEventLogger,
mRemoteInputManager,
mVisibilityLocationProviderDelegator,
- new SeenNotificationsProviderImpl(),
+ mSeenNotificationsProvider,
mShadeController,
mJankMonitor,
mStackLogger,
mLogger,
mNotificationStackSizeCalculator,
mFeatureFlags,
- mNotificationTargetsHelper
+ mNotificationTargetsHelper,
+ mSecureSettings
);
when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
@@ -233,16 +241,14 @@
mController.updateShowEmptyShadeView();
verify(mNotificationStackScrollLayout).updateEmptyShadeView(
/* visible= */ true,
- /* notifVisibleInShade= */ true,
- /* areSeenNotifsFiltered= */false);
+ /* notifVisibleInShade= */ true);
setupShowEmptyShadeViewState(false);
reset(mNotificationStackScrollLayout);
mController.updateShowEmptyShadeView();
verify(mNotificationStackScrollLayout).updateEmptyShadeView(
/* visible= */ false,
- /* notifVisibleInShade= */ true,
- /* areSeenNotifsFiltered= */false);
+ /* notifVisibleInShade= */ true);
}
@Test
@@ -255,16 +261,14 @@
mController.updateShowEmptyShadeView();
verify(mNotificationStackScrollLayout).updateEmptyShadeView(
/* visible= */ true,
- /* notifVisibleInShade= */ false,
- /* areSeenNotifsFiltered= */false);
+ /* notifVisibleInShade= */ false);
setupShowEmptyShadeViewState(false);
reset(mNotificationStackScrollLayout);
mController.updateShowEmptyShadeView();
verify(mNotificationStackScrollLayout).updateEmptyShadeView(
/* visible= */ false,
- /* notifVisibleInShade= */ false,
- /* areSeenNotifsFiltered= */false);
+ /* notifVisibleInShade= */ false);
}
@Test
@@ -283,16 +287,14 @@
mController.updateShowEmptyShadeView();
verify(mNotificationStackScrollLayout).updateEmptyShadeView(
/* visible= */ true,
- /* notifVisibleInShade= */ false,
- /* areSeenNotifsFiltered= */false);
+ /* notifVisibleInShade= */ false);
mController.setQsFullScreen(true);
reset(mNotificationStackScrollLayout);
mController.updateShowEmptyShadeView();
verify(mNotificationStackScrollLayout).updateEmptyShadeView(
/* visible= */ true,
- /* notifVisibleInShade= */ false,
- /* areSeenNotifsFiltered= */false);
+ /* notifVisibleInShade= */ false);
}
@Test
@@ -400,6 +402,17 @@
verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true);
}
+ @Test
+ public void testSetNotifStats_updatesHasFilteredOutSeenNotifications() {
+ when(mNotifPipelineFlags.getShouldFilterUnseenNotifsOnKeyguard()).thenReturn(true);
+ mSeenNotificationsProvider.setHasFilteredOutSeenNotifications(true);
+ mController.attach(mNotificationStackScrollLayout);
+ mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
+ verify(mNotificationStackScrollLayout).setHasFilteredOutSeenNotifications(true);
+ verify(mNotificationStackScrollLayout).updateFooter();
+ verify(mNotificationStackScrollLayout).updateEmptyShadeView(anyBoolean(), anyBoolean());
+ }
+
private LogMaker logMatcher(int category, int type) {
return argThat(new LogMatcher(category, type));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 7622549..dd7143a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -30,6 +30,7 @@
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
+import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -53,6 +54,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.TextView;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;
@@ -328,7 +330,7 @@
public void updateEmptyView_dndSuppressing() {
when(mEmptyShadeView.willBeGone()).thenReturn(true);
- mStackScroller.updateEmptyShadeView(true, true, false);
+ mStackScroller.updateEmptyShadeView(true, true);
verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
}
@@ -338,7 +340,7 @@
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mEmptyShadeView.willBeGone()).thenReturn(true);
- mStackScroller.updateEmptyShadeView(true, false, false);
+ mStackScroller.updateEmptyShadeView(true, false);
verify(mEmptyShadeView).setText(R.string.empty_shade_text);
}
@@ -347,10 +349,10 @@
public void updateEmptyView_noNotificationsToDndSuppressing() {
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mEmptyShadeView.willBeGone()).thenReturn(true);
- mStackScroller.updateEmptyShadeView(true, false, false);
+ mStackScroller.updateEmptyShadeView(true, false);
verify(mEmptyShadeView).setText(R.string.empty_shade_text);
- mStackScroller.updateEmptyShadeView(true, true, false);
+ mStackScroller.updateEmptyShadeView(true, true);
verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
}
@@ -818,6 +820,29 @@
assertEquals(0f, mAmbientState.getStackY());
}
+ @Test
+ public void hasFilteredOutSeenNotifs_updateFooter() {
+ mStackScroller.setCurrentUserSetup(true);
+
+ // add footer
+ mStackScroller.inflateFooterView();
+ TextView footerLabel =
+ mStackScroller.mFooterView.requireViewById(R.id.unlock_prompt_footer);
+
+ mStackScroller.setHasFilteredOutSeenNotifications(true);
+ mStackScroller.updateFooter();
+
+ assertThat(footerLabel.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void hasFilteredOutSeenNotifs_updateEmptyShadeView() {
+ mStackScroller.setHasFilteredOutSeenNotifications(true);
+ mStackScroller.updateEmptyShadeView(true, false);
+
+ verify(mEmptyShadeView).setFooterText(not(0));
+ }
+
private void setBarStateForTest(int state) {
// Can't inject this through the listener or we end up on the actual implementation
// rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
index 4ccbc6d..8cfcc07 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
@@ -24,6 +24,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
@@ -74,6 +75,7 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
+import org.mockito.stubbing.Answer;
import java.util.Collections;
import java.util.List;
@@ -115,8 +117,10 @@
@Spy private PackageManager mPackageManager;
private final boolean mIsReduceBrightColorsAvailable = true;
- private AutoTileManager mAutoTileManager;
+ private AutoTileManager mAutoTileManager; // under test
+
private SecureSettings mSecureSettings;
+ private ManagedProfileController.Callback mManagedProfileCallback;
@Before
public void setUp() throws Exception {
@@ -303,7 +307,7 @@
InOrder inOrderManagedProfile = inOrder(mManagedProfileController);
inOrderManagedProfile.verify(mManagedProfileController).removeCallback(any());
- inOrderManagedProfile.verify(mManagedProfileController, never()).addCallback(any());
+ inOrderManagedProfile.verify(mManagedProfileController).addCallback(any());
if (ColorDisplayManager.isNightDisplayAvailable(mContext)) {
InOrder inOrderNightDisplay = inOrder(mNightDisplayListener);
@@ -504,6 +508,41 @@
}
@Test
+ public void managedProfileAdded_tileAdded() {
+ when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(false);
+ when(mAutoAddTracker.getRestoredTilePosition(eq("work"))).thenReturn(2);
+ mAutoTileManager = createAutoTileManager(mContext);
+ Mockito.doAnswer((Answer<Object>) invocation -> {
+ mManagedProfileCallback = invocation.getArgument(0);
+ return null;
+ }).when(mManagedProfileController).addCallback(any());
+ mAutoTileManager.init();
+ when(mManagedProfileController.hasActiveProfile()).thenReturn(true);
+
+ mManagedProfileCallback.onManagedProfileChanged();
+
+ verify(mQsTileHost, times(1)).addTile(eq("work"), eq(2));
+ verify(mAutoAddTracker, times(1)).setTileAdded(eq("work"));
+ }
+
+ @Test
+ public void managedProfileRemoved_tileRemoved() {
+ when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(true);
+ mAutoTileManager = createAutoTileManager(mContext);
+ Mockito.doAnswer((Answer<Object>) invocation -> {
+ mManagedProfileCallback = invocation.getArgument(0);
+ return null;
+ }).when(mManagedProfileController).addCallback(any());
+ mAutoTileManager.init();
+ when(mManagedProfileController.hasActiveProfile()).thenReturn(false);
+
+ mManagedProfileCallback.onManagedProfileChanged();
+
+ verify(mQsTileHost, times(1)).removeTile(eq("work"));
+ verify(mAutoAddTracker, times(1)).setTileRemoved(eq("work"));
+ }
+
+ @Test
public void testEmptyArray_doesNotCrash() {
mContext.getOrCreateTestableResources().addOverride(
R.array.config_quickSettingsAutoAdd, new String[0]);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 74f8c61..daf7dd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -57,6 +59,7 @@
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
@@ -122,6 +125,7 @@
private VibratorHelper mVibratorHelper;
@Mock
private BiometricUnlockLogger mLogger;
+ private final FakeSystemClock mSystemClock = new FakeSystemClock();
private BiometricUnlockController mBiometricUnlockController;
@Before
@@ -144,7 +148,9 @@
mMetricsLogger, mDumpManager, mPowerManager, mLogger,
mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController,
- mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper);
+ mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
+ mSystemClock
+ );
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener);
when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
@@ -207,7 +213,7 @@
verify(mKeyguardViewMediator).onWakeAndUnlocking();
assertThat(mBiometricUnlockController.getMode())
- .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
+ .isEqualTo(MODE_WAKE_AND_UNLOCK);
}
@Test
@@ -457,4 +463,83 @@
// THEN wakeup the device
verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
}
+
+ @Test
+ public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() {
+ // GIVEN side fingerprint enrolled, last wake reason was power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+
+ // GIVEN last wake time just occurred
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN DO NOT vibrate the device
+ verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString());
+ }
+
+ @Test
+ public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
+ // GIVEN side fingerprint enrolled, last wake reason was power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+
+ // GIVEN last wake time was 500ms ago
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+ mSystemClock.advanceTime(500);
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).vibrateAuthSuccess(anyString());
+ }
+
+ @Test
+ public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() {
+ // GIVEN side fingerprint enrolled, wakeup just happened
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // GIVEN last wake reason was from a gesture
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_GESTURE);
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).vibrateAuthSuccess(anyString());
+ }
+
+ @Test
+ public void onSideFingerprintFail_alwaysPlaysHaptic() {
+ // GIVEN side fingerprint enrolled, last wake reason was recent power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // WHEN biometric fingerprint fails
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+
+ // THEN always vibrate the device
+ verify(mVibratorHelper).vibrateAuthError(anyString());
+ }
+
+ private void givenFingerprintModeUnlockCollapsing() {
+ when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+ when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index 3fccd37..b879cf2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -25,8 +25,10 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.StatusBarManager;
import android.os.PowerManager;
+import android.os.UserHandle;
import android.os.Vibrator;
import android.testing.AndroidTestingRunner;
import android.view.InsetsVisibilities;
@@ -41,6 +43,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.CameraLauncher;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.ShadeController;
@@ -88,6 +91,7 @@
@Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@Mock private SystemBarAttributesListener mSystemBarAttributesListener;
@Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
+ @Mock private UserTracker mUserTracker;
CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
@@ -120,8 +124,11 @@
new DisableFlagsLogger(),
DEFAULT_DISPLAY,
mSystemBarAttributesListener,
- mCameraLauncherLazy);
+ mCameraLauncherLazy,
+ mUserTracker);
+ when(mUserTracker.getUserHandle()).thenReturn(
+ UserHandle.of(ActivityManager.getCurrentUser()));
when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt()))
.thenAnswer((Answer<Integer>) invocation -> invocation.getArgument(0));
@@ -140,7 +147,7 @@
// Trying to open it does nothing.
mSbcqCallbacks.animateExpandNotificationsPanel();
- verify(mNotificationPanelViewController, never()).expandWithoutQs();
+ verify(mNotificationPanelViewController, never()).expandShadeToNotifications();
mSbcqCallbacks.animateExpandSettingsPanel(null);
verify(mNotificationPanelViewController, never()).expand(anyBoolean());
}
@@ -158,7 +165,7 @@
// Can now be opened.
mSbcqCallbacks.animateExpandNotificationsPanel();
- verify(mNotificationPanelViewController).expandWithoutQs();
+ verify(mNotificationPanelViewController).expandShadeToNotifications();
mSbcqCallbacks.animateExpandSettingsPanel(null);
verify(mNotificationPanelViewController).expandWithQs();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 09254ad..0605398 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -44,6 +44,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.IWallpaperManager;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -110,6 +111,7 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
@@ -117,6 +119,7 @@
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.settings.brightness.BrightnessSliderController;
import com.android.systemui.shade.CameraLauncher;
import com.android.systemui.shade.NotificationPanelView;
@@ -177,8 +180,6 @@
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.startingsurface.StartingSurface;
-import dagger.Lazy;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -191,6 +192,8 @@
import java.io.PrintWriter;
import java.util.Optional;
+import dagger.Lazy;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@@ -297,6 +300,7 @@
@Mock private WiredChargingRippleController mWiredChargingRippleController;
@Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
@Mock private CameraLauncher mCameraLauncher;
+ @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
/**
* The process of registering/unregistering a predictive back callback requires a
* ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test.
@@ -304,6 +308,7 @@
*/
@Mock private ViewRootImpl mViewRootImpl;
@Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
+ @Mock private UserTracker mUserTracker;
@Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
@Mock IPowerManager mPowerManagerService;
@@ -319,6 +324,10 @@
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
+ // CentralSurfacesImpl's runtime flag check fails if the flag is absent.
+ // This value is unused, because test manifest is opted in.
+ mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI, false);
+
IThermalService thermalService = mock(IThermalService.class);
mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,
Handler.createAsync(Looper.myLooper()));
@@ -336,7 +345,8 @@
new Handler(TestableLooper.get(this).getLooper()),
mock(NotifPipelineFlags.class),
mock(KeyguardNotificationVisibilityProvider.class),
- mock(UiEventLogger.class));
+ mock(UiEventLogger.class),
+ mUserTracker);
mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -378,7 +388,8 @@
}).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any());
mWakefulnessLifecycle =
- new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager);
+ new WakefulnessLifecycle(mContext, mIWallpaperManager, mFakeSystemClock,
+ mDumpManager);
mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
mWakefulnessLifecycle.dispatchFinishedWakingUp();
@@ -416,6 +427,9 @@
when(mOperatorNameViewControllerFactory.create(any()))
.thenReturn(mOperatorNameViewController);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+ when(mUserTracker.getUserHandle()).thenReturn(
+ UserHandle.of(ActivityManager.getCurrentUser()));
mCentralSurfaces = new CentralSurfacesImpl(
mContext,
@@ -504,7 +518,10 @@
mWiredChargingRippleController,
mDreamManager,
mCameraLauncherLazy,
- () -> mLightRevealScrimViewModel) {
+ () -> mLightRevealScrimViewModel,
+ mAlternateBouncerInteractor,
+ mUserTracker
+ ) {
@Override
protected ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -539,6 +556,7 @@
mCentralSurfaces.startKeyguard();
mInitController.executePostInitTasks();
notificationLogger.setUpWithContainer(mNotificationListContainer);
+ mCentralSurfaces.registerCallbacks();
}
@Test
@@ -992,6 +1010,18 @@
}
@Test
+ public void testSetDozingNotUnlocking_transitionToAOD_cancelKeyguardFadingAway() {
+ setDozing(true);
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true);
+
+ mCentralSurfaces.updateScrimController();
+
+ verify(mScrimController, times(2)).transitionTo(eq(ScrimState.AOD));
+ verify(mStatusBarKeyguardViewManager).onKeyguardFadedAway();
+ }
+
+ @Test
public void testShowKeyguardImplementation_setsState() {
when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>());
@@ -1274,7 +1304,8 @@
Handler mainHandler,
NotifPipelineFlags flags,
KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
- UiEventLogger uiEventLogger) {
+ UiEventLogger uiEventLogger,
+ UserTracker userTracker) {
super(
contentResolver,
powerManager,
@@ -1288,7 +1319,8 @@
mainHandler,
flags,
keyguardNotificationVisibilityProvider,
- uiEventLogger
+ uiEventLogger,
+ userTracker
);
mUseHeadsUp = true;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
index 077b41a..eb5edbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
@@ -23,8 +23,13 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.content.res.Resources;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Handler;
@@ -39,10 +44,10 @@
import com.android.systemui.doze.AlwaysOnDisplayPolicy;
import com.android.systemui.doze.DozeScreenState;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.unfold.FoldAodAnimationController;
@@ -52,6 +57,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -69,7 +76,6 @@
@Mock private PowerManager mPowerManager;
@Mock private TunerService mTunerService;
@Mock private BatteryController mBatteryController;
- @Mock private FeatureFlags mFeatureFlags;
@Mock private DumpManager mDumpManager;
@Mock private ScreenOffAnimationController mScreenOffAnimationController;
@Mock private FoldAodAnimationController mFoldAodAnimationController;
@@ -78,6 +84,8 @@
@Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Mock private StatusBarStateController mStatusBarStateController;
@Mock private ConfigurationController mConfigurationController;
+ @Mock private UserTracker mUserTracker;
+ @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback;
/**
* The current value of PowerManager's dozeAfterScreenOff property.
@@ -102,6 +110,7 @@
when(mSysUIUnfoldComponent.getFoldAodAnimationController())
.thenReturn(mFoldAodAnimationController);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
mDozeParameters = new DozeParameters(
mContext,
@@ -113,16 +122,17 @@
mBatteryController,
mTunerService,
mDumpManager,
- mFeatureFlags,
mScreenOffAnimationController,
Optional.of(mSysUIUnfoldComponent),
mUnlockedScreenOffAnimationController,
mKeyguardUpdateMonitor,
mConfigurationController,
- mStatusBarStateController
+ mStatusBarStateController,
+ mUserTracker
);
- when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(true);
+ verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture());
+
setAodEnabledForTest(true);
setShouldControlUnlockedScreenOffForTest(true);
setDisplayNeedsBlankingForTest(false);
@@ -173,6 +183,29 @@
assertThat(mDozeParameters.getAlwaysOn()).isFalse();
}
+ @Test
+ public void testGetAlwaysOn_whenBatterySaverCallback() {
+ DozeParameters.Callback callback = mock(DozeParameters.Callback.class);
+ mDozeParameters.addCallback(callback);
+
+ when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true);
+ when(mBatteryController.isAodPowerSave()).thenReturn(true);
+
+ // Both lines should trigger an event
+ mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1");
+ mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true);
+
+ verify(callback, times(2)).onAlwaysOnChange();
+ assertThat(mDozeParameters.getAlwaysOn()).isFalse();
+
+ reset(callback);
+ when(mBatteryController.isAodPowerSave()).thenReturn(false);
+ mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true);
+
+ verify(callback).onAlwaysOnChange();
+ assertThat(mDozeParameters.getAlwaysOn()).isTrue();
+ }
+
/**
* PowerManager.setDozeAfterScreenOff(true) means we are not controlling screen off, and calling
* it with false means we are. Confusing, but sure - make sure that we call PowerManager with
@@ -196,17 +229,6 @@
}
@Test
- public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() {
- when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false);
-
- assertFalse(mDozeParameters.shouldControlUnlockedScreenOff());
-
- // Trigger the setter for the current value.
- mDozeParameters.setControlScreenOffAnimation(mDozeParameters.shouldControlScreenOff());
- assertFalse(mDozeParameters.shouldControlScreenOff());
- }
-
- @Test
public void propagatesAnimateScreenOff_noAlwaysOn() {
setAodEnabledForTest(false);
setDisplayNeedsBlankingForTest(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
deleted file mode 100644
index df7ee43..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * Copyright (C) 2017 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.statusbar.phone;
-
-import static com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN;
-import static com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE;
-
-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.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.hardware.biometrics.BiometricSourceType;
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardHostViewController;
-import com.android.keyguard.KeyguardSecurityModel;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.ViewMediatorCallback;
-import com.android.keyguard.dagger.KeyguardBouncerComponent;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.keyguard.DismissCallbackRegistry;
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class KeyguardBouncerTest extends SysuiTestCase {
-
- @Mock
- private FalsingCollector mFalsingCollector;
- @Mock
- private ViewMediatorCallback mViewMediatorCallback;
- @Mock
- private DismissCallbackRegistry mDismissCallbackRegistry;
- @Mock
- private KeyguardHostViewController mKeyguardHostViewController;
- @Mock
- private KeyguardBouncer.PrimaryBouncerExpansionCallback mExpansionCallback;
- @Mock
- private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- @Mock
- private KeyguardStateController mKeyguardStateController;
- @Mock
- private KeyguardBypassController mKeyguardBypassController;
- @Mock
- private Handler mHandler;
- @Mock
- private KeyguardSecurityModel mKeyguardSecurityModel;
- @Mock
- private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
- @Mock
- private KeyguardBouncerComponent mKeyguardBouncerComponent;
- private ViewGroup mContainer;
- @Rule
- public MockitoRule mRule = MockitoJUnit.rule();
- private Integer mRootVisibility = View.INVISIBLE;
- private KeyguardBouncer mBouncer;
-
- @Before
- public void setup() {
- allowTestableLooperAsMainThread();
- when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
- .thenReturn(KeyguardSecurityModel.SecurityMode.None);
- DejankUtils.setImmediate(true);
-
- mContainer = spy(new FrameLayout(getContext()));
- when(mKeyguardBouncerComponentFactory.create(mContainer)).thenReturn(
- mKeyguardBouncerComponent);
- when(mKeyguardBouncerComponent.getKeyguardHostViewController())
- .thenReturn(mKeyguardHostViewController);
-
- mBouncer = new KeyguardBouncer.Factory(getContext(), mViewMediatorCallback,
- mDismissCallbackRegistry, mFalsingCollector,
- mKeyguardStateController, mKeyguardUpdateMonitor,
- mKeyguardBypassController, mHandler, mKeyguardSecurityModel,
- mKeyguardBouncerComponentFactory)
- .create(mContainer, mExpansionCallback);
- }
-
- @Test
- public void testInflateView_doesntCrash() {
- mBouncer.inflateView();
- }
-
- @Test
- public void testShow_notifiesFalsingManager() {
- mBouncer.show(true);
- verify(mFalsingCollector).onBouncerShown();
-
- mBouncer.show(true, false);
- verifyNoMoreInteractions(mFalsingCollector);
- }
-
- /**
- * Regression test: Invisible bouncer when occluded.
- */
- @Test
- public void testShow_bouncerIsVisible() {
- // Expand notification panel as if we were in the keyguard.
- mBouncer.ensureView();
- mBouncer.setExpansion(1);
-
- reset(mKeyguardHostViewController);
-
- mBouncer.show(true);
- verify(mKeyguardHostViewController).setExpansion(0);
- }
-
- @Test
- public void testShow_notifiesVisibility() {
- mBouncer.show(true);
- verify(mKeyguardStateController).notifyBouncerShowing(eq(true));
- verify(mExpansionCallback).onStartingToShow();
-
- // Not called again when visible
- reset(mViewMediatorCallback);
- mBouncer.show(true);
- verifyNoMoreInteractions(mViewMediatorCallback);
- }
-
- @Test
- public void testShow_triesToDismissKeyguard() {
- mBouncer.show(true);
- verify(mKeyguardHostViewController).dismiss(anyInt());
- }
-
- @Test
- public void testShow_resetsSecuritySelection() {
- mBouncer.show(false);
- verify(mKeyguardHostViewController, never()).showPrimarySecurityScreen();
-
- mBouncer.hide(false);
- mBouncer.show(true);
- verify(mKeyguardHostViewController).showPrimarySecurityScreen();
- }
-
- @Test
- public void testShow_animatesKeyguardView() {
- mBouncer.show(true);
- verify(mKeyguardHostViewController).appear(anyInt());
- }
-
- @Test
- public void testShow_showsErrorMessage() {
- final String errorMessage = "an error message";
- when(mViewMediatorCallback.consumeCustomMessage()).thenReturn(errorMessage);
- mBouncer.show(true);
- verify(mKeyguardHostViewController).showErrorMessage(eq(errorMessage));
- }
-
- @Test
- public void testSetExpansion_notifiesFalsingManager() {
- mBouncer.ensureView();
- mBouncer.setExpansion(0.5f);
-
- mBouncer.setExpansion(EXPANSION_HIDDEN);
- verify(mFalsingCollector).onBouncerHidden();
- verify(mExpansionCallback).onFullyHidden();
-
- mBouncer.setExpansion(EXPANSION_VISIBLE);
- verify(mFalsingCollector).onBouncerShown();
- verify(mExpansionCallback).onFullyShown();
-
- verify(mExpansionCallback, never()).onStartingToHide();
- verify(mKeyguardHostViewController, never()).onStartingToHide();
- mBouncer.setExpansion(0.9f);
- verify(mExpansionCallback).onStartingToHide();
- verify(mKeyguardHostViewController).onStartingToHide();
- }
-
- @Test
- public void testSetExpansion_notifiesKeyguardView() {
- mBouncer.ensureView();
- mBouncer.setExpansion(0.1f);
-
- mBouncer.setExpansion(0);
- verify(mKeyguardHostViewController).onResume();
- verify(mContainer).announceForAccessibility(any());
- }
-
- @Test
- public void show_notifiesKeyguardViewController() {
- mBouncer.ensureView();
-
- mBouncer.show(/* resetSecuritySelection= */ false);
-
- verify(mKeyguardHostViewController).onBouncerVisibilityChanged(View.VISIBLE);
- }
-
- @Test
- public void testHide_notifiesFalsingManager() {
- mBouncer.hide(false);
- verify(mFalsingCollector).onBouncerHidden();
- }
-
- @Test
- public void testHide_notifiesVisibility() {
- mBouncer.hide(false);
- verify(mKeyguardStateController).notifyBouncerShowing(eq(false));
- }
-
- @Test
- public void testHide_notifiesDismissCallbackIfVisible() {
- mBouncer.hide(false);
- verifyZeroInteractions(mDismissCallbackRegistry);
- mBouncer.show(false);
- mBouncer.hide(false);
- verify(mDismissCallbackRegistry).notifyDismissCancelled();
- }
-
- @Test
- public void testHide_notShowingAnymore() {
- mBouncer.ensureView();
- mBouncer.show(false /* resetSecuritySelection */);
- mBouncer.hide(false /* destroyViews */);
- Assert.assertFalse("Not showing", mBouncer.isShowing());
- }
-
- @Test
- public void testShowPromptReason_propagates() {
- mBouncer.ensureView();
- mBouncer.showPromptReason(1);
- verify(mKeyguardHostViewController).showPromptReason(eq(1));
- }
-
- @Test
- public void testShowMessage_propagates() {
- final String message = "a message";
- mBouncer.ensureView();
- mBouncer.showMessage(message, ColorStateList.valueOf(Color.GREEN));
- verify(mKeyguardHostViewController).showMessage(
- eq(message), eq(ColorStateList.valueOf(Color.GREEN)));
- }
-
- @Test
- public void testShowOnDismissAction_showsBouncer() {
- final OnDismissAction dismissAction = () -> false;
- final Runnable cancelAction = () -> {};
- mBouncer.showWithDismissAction(dismissAction, cancelAction);
- verify(mKeyguardHostViewController).setOnDismissAction(dismissAction, cancelAction);
- Assert.assertTrue("Should be showing", mBouncer.isShowing());
- }
-
- @Test
- public void testStartPreHideAnimation_notifiesView() {
- final boolean[] ran = {false};
- final Runnable r = () -> ran[0] = true;
- mBouncer.startPreHideAnimation(r);
- Assert.assertTrue("Callback should have been invoked", ran[0]);
-
- ran[0] = false;
- mBouncer.ensureView();
- mBouncer.startPreHideAnimation(r);
- verify(mKeyguardHostViewController).startDisappearAnimation(r);
- Assert.assertFalse("Callback should have been deferred", ran[0]);
- }
-
- @Test
- public void testIsShowing_animated() {
- Assert.assertFalse("Show wasn't invoked yet", mBouncer.isShowing());
- mBouncer.show(true /* reset */);
- Assert.assertTrue("Should be showing", mBouncer.isShowing());
- }
-
- @Test
- public void testIsShowing_forSwipeUp() {
- mBouncer.setExpansion(1f);
- mBouncer.show(true /* reset */, false /* animated */);
- Assert.assertFalse("Should only be showing after collapsing notification panel",
- mBouncer.isShowing());
- mBouncer.setExpansion(0f);
- Assert.assertTrue("Should be showing", mBouncer.isShowing());
- }
-
- @Test
- public void testSetExpansion() {
- mBouncer.ensureView();
- mBouncer.setExpansion(0.5f);
- verify(mKeyguardHostViewController).setExpansion(0.5f);
- }
-
- @Test
- public void testIsFullscreenBouncer_asksKeyguardView() {
- mBouncer.ensureView();
- mBouncer.isFullscreenBouncer();
- verify(mKeyguardHostViewController).getCurrentSecurityMode();
- }
-
- @Test
- public void testIsHiding_preHideOrHide() {
- Assert.assertFalse("Should not be hiding on initial state", mBouncer.isAnimatingAway());
- mBouncer.startPreHideAnimation(null /* runnable */);
- Assert.assertTrue("Should be hiding during pre-hide", mBouncer.isAnimatingAway());
- mBouncer.hide(false /* destroyView */);
- Assert.assertFalse("Should be hidden after hide()", mBouncer.isAnimatingAway());
- }
-
- @Test
- public void testIsHiding_skipsTranslation() {
- mBouncer.show(false /* reset */);
- reset(mKeyguardHostViewController);
- mBouncer.startPreHideAnimation(null /* runnable */);
- mBouncer.setExpansion(0.5f);
- verify(mKeyguardHostViewController, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void testIsSecure() {
- mBouncer.ensureView();
- for (KeyguardSecurityModel.SecurityMode mode : KeyguardSecurityModel.SecurityMode.values()){
- reset(mKeyguardSecurityModel);
- when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(mode);
- Assert.assertEquals("Security doesn't match for mode: " + mode,
- mBouncer.isSecure(), mode != KeyguardSecurityModel.SecurityMode.None);
- }
- }
-
- @Test
- public void testIsShowingScrimmed_true() {
- doAnswer(invocation -> {
- assertThat(mBouncer.isScrimmed()).isTrue();
- return null;
- }).when(mExpansionCallback).onFullyShown();
- mBouncer.show(false /* resetSecuritySelection */, true /* animate */);
- assertThat(mBouncer.isScrimmed()).isTrue();
- mBouncer.hide(false /* destroyView */);
- assertThat(mBouncer.isScrimmed()).isFalse();
- }
-
- @Test
- public void testIsShowingScrimmed_false() {
- doAnswer(invocation -> {
- assertThat(mBouncer.isScrimmed()).isFalse();
- return null;
- }).when(mExpansionCallback).onFullyShown();
- mBouncer.show(false /* resetSecuritySelection */, false /* animate */);
- assertThat(mBouncer.isScrimmed()).isFalse();
- }
-
- @Test
- public void testWillDismissWithAction() {
- mBouncer.ensureView();
- Assert.assertFalse("Action not set yet", mBouncer.willDismissWithAction());
- when(mKeyguardHostViewController.hasDismissActions()).thenReturn(true);
- Assert.assertTrue("Action should exist", mBouncer.willDismissWithAction());
- }
-
- @Test
- public void testShow_delaysIfFaceAuthIsRunning() {
- when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
- .thenReturn(true);
- when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
- mBouncer.show(true /* reset */);
-
- ArgumentCaptor<Runnable> showRunnable = ArgumentCaptor.forClass(Runnable.class);
- verify(mHandler).postDelayed(showRunnable.capture(),
- eq(KeyguardBouncer.BOUNCER_FACE_DELAY));
-
- mBouncer.hide(false /* destroyView */);
- verify(mHandler).removeCallbacks(eq(showRunnable.getValue()));
- }
-
- @Test
- public void testShow_doesNotDelaysIfFaceAuthIsNotAllowed() {
- when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
- when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
- .thenReturn(false);
- mBouncer.show(true /* reset */);
-
- verify(mHandler, never()).postDelayed(any(), anyLong());
- }
-
- @Test
- public void testShow_delaysIfFaceAuthIsRunning_unlessBypassEnabled() {
- when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
- when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
- mBouncer.show(true /* reset */);
-
- verify(mHandler, never()).postDelayed(any(), anyLong());
- }
-
- @Test
- public void testShow_delaysIfFaceAuthIsRunning_unlessFingerprintEnrolled() {
- when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true);
- when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(0))
- .thenReturn(true);
- mBouncer.show(true /* reset */);
-
- verify(mHandler, never()).postDelayed(any(), anyLong());
- }
-
- @Test
- public void testRegisterUpdateMonitorCallback() {
- verify(mKeyguardUpdateMonitor).registerCallback(any());
- }
-
- @Test
- public void testInTransit_whenTranslation() {
- mBouncer.show(true);
- mBouncer.setExpansion(EXPANSION_HIDDEN);
- assertThat(mBouncer.inTransit()).isFalse();
- mBouncer.setExpansion(0.5f);
- assertThat(mBouncer.inTransit()).isTrue();
- mBouncer.setExpansion(EXPANSION_VISIBLE);
- assertThat(mBouncer.inTransit()).isFalse();
- }
-
- @Test
- public void testUpdateResources_delegatesToRootView() {
- mBouncer.ensureView();
- mBouncer.updateResources();
-
- // This is mocked, so won't pick up on the call to updateResources via
- // mKeyguardViewController.init(), only updateResources above.
- verify(mKeyguardHostViewController).updateResources();
- }
-
- @Test
- public void testUpdateKeyguardPosition_delegatesToRootView() {
- mBouncer.ensureView();
- mBouncer.updateKeyguardPosition(1.0f);
-
- verify(mKeyguardHostViewController).updateKeyguardPosition(1.0f);
- }
-
- @Test
- public void testExpansion_notifiesCallback() {
- mBouncer.ensureView();
- mBouncer.setExpansion(0.5f);
-
- final PrimaryBouncerExpansionCallback callback =
- mock(PrimaryBouncerExpansionCallback.class);
- mBouncer.addBouncerExpansionCallback(callback);
-
- mBouncer.setExpansion(EXPANSION_HIDDEN);
- verify(callback).onFullyHidden();
- verify(callback).onExpansionChanged(EXPANSION_HIDDEN);
-
- Mockito.clearInvocations(callback);
- mBouncer.setExpansion(EXPANSION_VISIBLE);
- verify(callback).onFullyShown();
- verify(callback).onExpansionChanged(EXPANSION_VISIBLE);
-
- Mockito.clearInvocations(callback);
- float bouncerHideAmount = 0.9f;
- // Ensure the callback only triggers once despite multiple calls to setExpansion
- // with the same value.
- mBouncer.setExpansion(bouncerHideAmount);
- mBouncer.setExpansion(bouncerHideAmount);
- verify(callback, times(1)).onStartingToHide();
- verify(callback, times(1)).onExpansionChanged(bouncerHideAmount);
-
- Mockito.clearInvocations(callback);
- mBouncer.removeBouncerExpansionCallback(callback);
- bouncerHideAmount = 0.5f;
- mBouncer.setExpansion(bouncerHideAmount);
- verify(callback, never()).onExpansionChanged(bouncerHideAmount);
- }
-
- @Test
- public void testOnResumeCalledForFullscreenBouncerOnSecondShow() {
- // GIVEN a security mode which requires fullscreen bouncer
- when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
- .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
- mBouncer.show(true);
-
- // WHEN a second call to show occurs, the bouncer will already by visible
- reset(mKeyguardHostViewController);
- mBouncer.show(true);
-
- // THEN ensure the ViewController is told to resume
- verify(mKeyguardHostViewController).onResume();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
new file mode 100644
index 0000000..3e90ed9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.statusbar.phone
+
+import android.content.pm.PackageManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_FLIPPED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.tuner.TunerService
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class KeyguardBypassControllerTest : SysuiTestCase() {
+
+ private lateinit var keyguardBypassController: KeyguardBypassController
+ private lateinit var postureControllerCallback: DevicePostureController.Callback
+ @Mock private lateinit var tunerService: TunerService
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+ @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+ @Mock private lateinit var keyguardStateController: KeyguardStateController
+ @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+ @Mock private lateinit var devicePostureController: DevicePostureController
+ @Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var packageManager: PackageManager
+ @Captor
+ private val postureCallbackCaptor =
+ ArgumentCaptor.forClass(DevicePostureController.Callback::class.java)
+ @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+ @Before
+ fun setUp() {
+ context.setMockPackageManager(packageManager)
+ whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true)
+ whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
+ }
+
+ @After
+ fun tearDown() {
+ reset(devicePostureController)
+ reset(keyguardStateController)
+ }
+
+ private fun defaultConfigPostureClosed() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_auth_supported_posture,
+ DEVICE_POSTURE_CLOSED
+ )
+ initKeyguardBypassController()
+ verify(devicePostureController).addCallback(postureCallbackCaptor.capture())
+ postureControllerCallback = postureCallbackCaptor.value
+ }
+
+ private fun defaultConfigPostureOpened() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_auth_supported_posture,
+ DEVICE_POSTURE_OPENED
+ )
+ initKeyguardBypassController()
+ verify(devicePostureController).addCallback(postureCallbackCaptor.capture())
+ postureControllerCallback = postureCallbackCaptor.value
+ }
+
+ private fun defaultConfigPostureFlipped() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_auth_supported_posture,
+ DEVICE_POSTURE_FLIPPED
+ )
+ initKeyguardBypassController()
+ verify(devicePostureController).addCallback(postureCallbackCaptor.capture())
+ postureControllerCallback = postureCallbackCaptor.value
+ }
+
+ private fun defaultConfigPostureUnknown() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_auth_supported_posture,
+ DEVICE_POSTURE_UNKNOWN
+ )
+ initKeyguardBypassController()
+ verify(devicePostureController, never()).addCallback(postureCallbackCaptor.capture())
+ }
+
+ private fun initKeyguardBypassController() {
+ keyguardBypassController =
+ KeyguardBypassController(
+ context,
+ tunerService,
+ statusBarStateController,
+ lockscreenUserManager,
+ keyguardStateController,
+ shadeExpansionStateManager,
+ devicePostureController,
+ dumpManager
+ )
+ }
+
+ @Test
+ fun configDevicePostureClosed_matchState_isPostureAllowedForFaceAuth_returnTrue() {
+ defaultConfigPostureClosed()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue()
+ }
+
+ @Test
+ fun configDevicePostureOpen_matchState_isPostureAllowedForFaceAuth_returnTrue() {
+ defaultConfigPostureOpened()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue()
+ }
+
+ @Test
+ fun configDevicePostureFlipped_matchState_isPostureAllowedForFaceAuth_returnTrue() {
+ defaultConfigPostureFlipped()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isTrue()
+ }
+
+ @Test
+ fun configDevicePostureClosed_changeOpened_isPostureAllowedForFaceAuth_returnFalse() {
+ defaultConfigPostureClosed()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+ }
+
+ @Test
+ fun configDevicePostureClosed_changeFlipped_isPostureAllowedForFaceAuth_returnFalse() {
+ defaultConfigPostureClosed()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+ }
+
+ @Test
+ fun configDevicePostureOpened_changeClosed_isPostureAllowedForFaceAuth_returnFalse() {
+ defaultConfigPostureOpened()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+ }
+
+ @Test
+ fun configDevicePostureOpened_changeFlipped_isPostureAllowedForFaceAuth_returnFalse() {
+ defaultConfigPostureOpened()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_FLIPPED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+ }
+
+ @Test
+ fun configDevicePostureFlipped_changeClosed_isPostureAllowedForFaceAuth_returnFalse() {
+ defaultConfigPostureFlipped()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+ }
+
+ @Test
+ fun configDevicePostureFlipped_changeOpened_isPostureAllowedForFaceAuth_returnFalse() {
+ defaultConfigPostureFlipped()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+ assertThat(keyguardBypassController.isPostureAllowedForFaceAuth()).isFalse()
+ }
+
+ @Test
+ fun defaultConfigPostureClosed_canOverrideByPassAlways_shouldReturnFalse() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_unlock_bypass_override,
+ 1 /* FACE_UNLOCK_BYPASS_ALWAYS */
+ )
+
+ defaultConfigPostureClosed()
+
+ postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+ assertThat(keyguardBypassController.bypassEnabled).isFalse()
+ }
+
+ @Test
+ fun defaultConfigPostureUnknown_canNotOverrideByPassAlways_shouldReturnTrue() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_unlock_bypass_override,
+ 1 /* FACE_UNLOCK_BYPASS_ALWAYS */
+ )
+
+ defaultConfigPostureUnknown()
+
+ assertThat(keyguardBypassController.bypassEnabled).isTrue()
+ }
+
+ @Test
+ fun defaultConfigPostureUnknown_canNotOverrideByPassNever_shouldReturnFalse() {
+ context.orCreateTestableResources.addOverride(
+ R.integer.config_face_unlock_bypass_override,
+ 2 /* FACE_UNLOCK_BYPASS_NEVER */
+ )
+
+ defaultConfigPostureUnknown()
+
+ assertThat(keyguardBypassController.bypassEnabled).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index cc4abfc..529519a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -38,6 +38,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.policy.BatteryController;
import org.junit.Before;
@@ -67,7 +68,8 @@
mStatusBarIconController,
mock(BatteryController.class),
mock(NavigationModeController.class),
- mock(DumpManager.class));
+ mock(DumpManager.class),
+ new FakeDisplayTracker(mContext));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
index 189aa0f..0a68406 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
@@ -28,6 +28,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.LightBarTransitionsController.DarkIntensityApplier;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -56,7 +57,8 @@
public void setup() {
MockitoAnnotations.initMocks(this);
mLightBarTransitionsController = new LightBarTransitionsController(mContext, mApplier,
- new CommandQueue(mContext), mKeyguardStateController, mStatusBarStateController);
+ new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
+ mKeyguardStateController, mStatusBarStateController);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index 64dee95..305b9fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -31,6 +31,7 @@
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.screenrecord.RecordingController
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.BluetoothController
import com.android.systemui.statusbar.policy.CastController
@@ -71,61 +72,37 @@
private const val ALARM_SLOT = "alarm"
}
- @Mock
- private lateinit var iconController: StatusBarIconController
- @Mock
- private lateinit var commandQueue: CommandQueue
- @Mock
- private lateinit var broadcastDispatcher: BroadcastDispatcher
- @Mock
- private lateinit var castController: CastController
- @Mock
- private lateinit var hotspotController: HotspotController
- @Mock
- private lateinit var bluetoothController: BluetoothController
- @Mock
- private lateinit var nextAlarmController: NextAlarmController
- @Mock
- private lateinit var userInfoController: UserInfoController
- @Mock
- private lateinit var rotationLockController: RotationLockController
- @Mock
- private lateinit var dataSaverController: DataSaverController
- @Mock
- private lateinit var zenModeController: ZenModeController
- @Mock
- private lateinit var deviceProvisionedController: DeviceProvisionedController
- @Mock
- private lateinit var keyguardStateController: KeyguardStateController
- @Mock
- private lateinit var locationController: LocationController
- @Mock
- private lateinit var sensorPrivacyController: SensorPrivacyController
- @Mock
- private lateinit var iActivityManager: IActivityManager
- @Mock
- private lateinit var alarmManager: AlarmManager
- @Mock
- private lateinit var userManager: UserManager
- @Mock
- private lateinit var devicePolicyManager: DevicePolicyManager
- @Mock
- private lateinit var recordingController: RecordingController
- @Mock
- private lateinit var telecomManager: TelecomManager
- @Mock
- private lateinit var sharedPreferences: SharedPreferences
- @Mock
- private lateinit var dateFormatUtil: DateFormatUtil
+ @Mock private lateinit var iconController: StatusBarIconController
+ @Mock private lateinit var commandQueue: CommandQueue
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var castController: CastController
+ @Mock private lateinit var hotspotController: HotspotController
+ @Mock private lateinit var bluetoothController: BluetoothController
+ @Mock private lateinit var nextAlarmController: NextAlarmController
+ @Mock private lateinit var userInfoController: UserInfoController
+ @Mock private lateinit var rotationLockController: RotationLockController
+ @Mock private lateinit var dataSaverController: DataSaverController
+ @Mock private lateinit var zenModeController: ZenModeController
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var keyguardStateController: KeyguardStateController
+ @Mock private lateinit var locationController: LocationController
+ @Mock private lateinit var sensorPrivacyController: SensorPrivacyController
+ @Mock private lateinit var iActivityManager: IActivityManager
+ @Mock private lateinit var alarmManager: AlarmManager
+ @Mock private lateinit var userManager: UserManager
+ @Mock private lateinit var userTracker: UserTracker
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var recordingController: RecordingController
+ @Mock private lateinit var telecomManager: TelecomManager
+ @Mock private lateinit var sharedPreferences: SharedPreferences
+ @Mock private lateinit var dateFormatUtil: DateFormatUtil
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private lateinit var ringerModeTracker: RingerModeTracker
- @Mock
- private lateinit var privacyItemController: PrivacyItemController
- @Mock
- private lateinit var privacyLogger: PrivacyLogger
+ @Mock private lateinit var privacyItemController: PrivacyItemController
+ @Mock private lateinit var privacyLogger: PrivacyLogger
@Captor
private lateinit var alarmCallbackCaptor:
- ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>
+ ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>
private lateinit var executor: FakeExecutor
private lateinit var statusBarPolicy: PhoneStatusBarPolicy
@@ -137,8 +114,8 @@
executor = FakeExecutor(FakeSystemClock())
testableLooper = TestableLooper.get(this)
context.orCreateTestableResources.addOverride(
- com.android.internal.R.string.status_bar_alarm_clock,
- ALARM_SLOT
+ com.android.internal.R.string.status_bar_alarm_clock,
+ ALARM_SLOT
)
statusBarPolicy = createStatusBarPolicy()
}
@@ -195,36 +172,37 @@
private fun createStatusBarPolicy(): PhoneStatusBarPolicy {
return PhoneStatusBarPolicy(
- iconController,
- commandQueue,
- broadcastDispatcher,
- executor,
- testableLooper.looper,
- context.resources,
- castController,
- hotspotController,
- bluetoothController,
- nextAlarmController,
- userInfoController,
- rotationLockController,
- dataSaverController,
- zenModeController,
- deviceProvisionedController,
- keyguardStateController,
- locationController,
- sensorPrivacyController,
- iActivityManager,
- alarmManager,
- userManager,
- devicePolicyManager,
- recordingController,
- telecomManager,
- /* displayId = */ 0,
- sharedPreferences,
- dateFormatUtil,
- ringerModeTracker,
- privacyItemController,
- privacyLogger
+ iconController,
+ commandQueue,
+ broadcastDispatcher,
+ executor,
+ testableLooper.looper,
+ context.resources,
+ castController,
+ hotspotController,
+ bluetoothController,
+ nextAlarmController,
+ userInfoController,
+ rotationLockController,
+ dataSaverController,
+ zenModeController,
+ deviceProvisionedController,
+ keyguardStateController,
+ locationController,
+ sensorPrivacyController,
+ iActivityManager,
+ alarmManager,
+ userManager,
+ userTracker,
+ devicePolicyManager,
+ recordingController,
+ telecomManager,
+ /* displayId = */ 0,
+ sharedPreferences,
+ dateFormatUtil,
+ ringerModeTracker,
+ privacyItemController,
+ privacyLogger
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index e475905..c0537a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -58,6 +58,7 @@
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.statusbar.policy.FakeConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -629,6 +630,7 @@
@Test
public void transitionToUnlocked() {
+ mScrimController.setClipsQsScrim(false);
mScrimController.setRawPanelExpansionFraction(0f);
mScrimController.transitionTo(ScrimState.UNLOCKED);
finishAnimationsImmediately();
@@ -644,14 +646,21 @@
));
// Back scrim should be visible after start dragging
- mScrimController.setRawPanelExpansionFraction(0.3f);
+ mScrimController.setRawPanelExpansionFraction(0.29f);
assertScrimAlpha(Map.of(
mScrimInFront, TRANSPARENT,
mNotificationsScrim, TRANSPARENT,
mScrimBehind, SEMI_TRANSPARENT));
+ // Back scrim should be opaque at 30%
+ mScrimController.setRawPanelExpansionFraction(0.3f);
+ assertScrimAlpha(Map.of(
+ mScrimInFront, TRANSPARENT,
+ mNotificationsScrim, TRANSPARENT,
+ mScrimBehind, OPAQUE));
+
// Then, notification scrim should fade in
- mScrimController.setRawPanelExpansionFraction(0.7f);
+ mScrimController.setRawPanelExpansionFraction(0.31f);
assertScrimAlpha(Map.of(
mScrimInFront, TRANSPARENT,
mNotificationsScrim, SEMI_TRANSPARENT,
@@ -1562,7 +1571,7 @@
@Test
public void transitionToDreaming() {
mScrimController.setRawPanelExpansionFraction(0f);
- mScrimController.setBouncerHiddenFraction(KeyguardBouncer.EXPANSION_HIDDEN);
+ mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
mScrimController.transitionTo(ScrimState.DREAMING);
finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
new file mode 100644
index 0000000..3bc288a2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
@@ -0,0 +1,309 @@
+/*
+ * 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.statusbar.phone
+
+import android.os.UserHandle
+import androidx.test.filters.SmallTest
+import com.android.internal.statusbar.StatusBarIcon
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY
+import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@SmallTest
+class StatusBarIconControllerImplTest : SysuiTestCase() {
+
+ private lateinit var underTest: StatusBarIconControllerImpl
+
+ private lateinit var iconList: StatusBarIconList
+ private val iconGroup: StatusBarIconController.IconManager = mock()
+
+ @Before
+ fun setUp() {
+ iconList = StatusBarIconList(arrayOf())
+ underTest =
+ StatusBarIconControllerImpl(
+ context,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ iconList,
+ mock(),
+ )
+ underTest.addIconGroup(iconGroup)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_bothDisplayed() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ val externalIcon =
+ StatusBarIcon(
+ "external.package",
+ UserHandle.ALL,
+ /* iconId= */ 2,
+ /* iconLevel= */ 0,
+ /* number= */ 0,
+ "contentDescription",
+ )
+ underTest.setIcon(slotName, externalIcon)
+
+ assertThat(iconList.slots).hasSize(2)
+ // Whichever was added last comes first
+ assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(iconList.slots[1].name).isEqualTo(slotName)
+ assertThat(iconList.slots[0].hasIconsInSlot()).isTrue()
+ assertThat(iconList.slots[1].hasIconsInSlot()).isTrue()
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_externalRemoved_viaRemoveIcon_internalStays() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ underTest.setIcon(slotName, createExternalIcon())
+
+ // WHEN the external icon is removed via #removeIcon
+ underTest.removeIcon(slotName)
+
+ // THEN the external icon is removed but the internal icon remains
+ // Note: [StatusBarIconList] never removes slots from its list, it just sets the holder for
+ // the slot to null when an icon is removed.
+ assertThat(iconList.slots).hasSize(2)
+ assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(iconList.slots[1].name).isEqualTo(slotName)
+ assertThat(iconList.slots[0].hasIconsInSlot()).isFalse() // Indicates removal
+ assertThat(iconList.slots[1].hasIconsInSlot()).isTrue()
+
+ verify(iconGroup).onRemoveIcon(0)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_externalRemoved_viaRemoveAll_internalStays() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ underTest.setIcon(slotName, createExternalIcon())
+
+ // WHEN the external icon is removed via #removeAllIconsForExternalSlot
+ underTest.removeAllIconsForExternalSlot(slotName)
+
+ // THEN the external icon is removed but the internal icon remains
+ assertThat(iconList.slots).hasSize(2)
+ assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(iconList.slots[1].name).isEqualTo(slotName)
+ assertThat(iconList.slots[0].hasIconsInSlot()).isFalse() // Indicates removal
+ assertThat(iconList.slots[1].hasIconsInSlot()).isTrue()
+
+ verify(iconGroup).onRemoveIcon(0)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_externalRemoved_viaSetNull_internalStays() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ underTest.setIcon(slotName, createExternalIcon())
+
+ // WHEN the external icon is removed via a #setIcon(null)
+ underTest.setIcon(slotName, /* icon= */ null)
+
+ // THEN the external icon is removed but the internal icon remains
+ assertThat(iconList.slots).hasSize(2)
+ assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(iconList.slots[1].name).isEqualTo(slotName)
+ assertThat(iconList.slots[0].hasIconsInSlot()).isFalse() // Indicates removal
+ assertThat(iconList.slots[1].hasIconsInSlot()).isTrue()
+
+ verify(iconGroup).onRemoveIcon(0)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_internalRemoved_viaRemove_externalStays() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ underTest.setIcon(slotName, createExternalIcon())
+
+ // WHEN the internal icon is removed via #removeIcon
+ underTest.removeIcon(slotName, /* tag= */ 0)
+
+ // THEN the external icon is removed but the internal icon remains
+ assertThat(iconList.slots).hasSize(2)
+ assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(iconList.slots[1].name).isEqualTo(slotName)
+ assertThat(iconList.slots[0].hasIconsInSlot()).isTrue()
+ assertThat(iconList.slots[1].hasIconsInSlot()).isFalse() // Indicates removal
+
+ verify(iconGroup).onRemoveIcon(1)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_internalRemoved_viaRemoveAll_externalStays() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ underTest.setIcon(slotName, createExternalIcon())
+
+ // WHEN the internal icon is removed via #removeAllIconsForSlot
+ underTest.removeAllIconsForSlot(slotName)
+
+ // THEN the external icon is removed but the internal icon remains
+ assertThat(iconList.slots).hasSize(2)
+ assertThat(iconList.slots[0].name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(iconList.slots[1].name).isEqualTo(slotName)
+ assertThat(iconList.slots[0].hasIconsInSlot()).isTrue()
+ assertThat(iconList.slots[1].hasIconsInSlot()).isFalse() // Indicates removal
+
+ verify(iconGroup).onRemoveIcon(1)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_internalUpdatedIndependently() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ val startingExternalIcon =
+ StatusBarIcon(
+ "external.package",
+ UserHandle.ALL,
+ /* iconId= */ 20,
+ /* iconLevel= */ 0,
+ /* number= */ 0,
+ "externalDescription",
+ )
+ underTest.setIcon(slotName, startingExternalIcon)
+
+ // WHEN the internal icon is updated
+ underTest.setIcon(slotName, /* resourceId= */ 11, "newContentDescription")
+
+ // THEN only the internal slot gets the updates
+ val internalSlot = iconList.slots[1]
+ val internalHolder = internalSlot.getHolderForTag(TAG_PRIMARY)!!
+ assertThat(internalSlot.name).isEqualTo(slotName)
+ assertThat(internalHolder.icon!!.contentDescription).isEqualTo("newContentDescription")
+ assertThat(internalHolder.icon!!.icon.resId).isEqualTo(11)
+
+ // And the external slot has its own values
+ val externalSlot = iconList.slots[0]
+ val externalHolder = externalSlot.getHolderForTag(TAG_PRIMARY)!!
+ assertThat(externalSlot.name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(externalHolder.icon!!.contentDescription).isEqualTo("externalDescription")
+ assertThat(externalHolder.icon!!.icon.resId).isEqualTo(20)
+ }
+
+ /** Regression test for b/255428281. */
+ @Test
+ fun internalAndExternalIconWithSameName_externalUpdatedIndependently() {
+ val slotName = "mute"
+
+ // Internal
+ underTest.setIcon(slotName, /* resourceId= */ 10, "contentDescription")
+
+ // External
+ val startingExternalIcon =
+ StatusBarIcon(
+ "external.package",
+ UserHandle.ALL,
+ /* iconId= */ 20,
+ /* iconLevel= */ 0,
+ /* number= */ 0,
+ "externalDescription",
+ )
+ underTest.setIcon(slotName, startingExternalIcon)
+
+ // WHEN the external icon is updated
+ val newExternalIcon =
+ StatusBarIcon(
+ "external.package",
+ UserHandle.ALL,
+ /* iconId= */ 21,
+ /* iconLevel= */ 0,
+ /* number= */ 0,
+ "newExternalDescription",
+ )
+ underTest.setIcon(slotName, newExternalIcon)
+
+ // THEN only the external slot gets the updates
+ val externalSlot = iconList.slots[0]
+ val externalHolder = externalSlot.getHolderForTag(TAG_PRIMARY)!!
+ assertThat(externalSlot.name).isEqualTo(slotName + EXTERNAL_SLOT_SUFFIX)
+ assertThat(externalHolder.icon!!.contentDescription).isEqualTo("newExternalDescription")
+ assertThat(externalHolder.icon!!.icon.resId).isEqualTo(21)
+
+ // And the internal slot has its own values
+ val internalSlot = iconList.slots[1]
+ val internalHolder = internalSlot.getHolderForTag(TAG_PRIMARY)!!
+ assertThat(internalSlot.name).isEqualTo(slotName)
+ assertThat(internalHolder.icon!!.contentDescription).isEqualTo("contentDescription")
+ assertThat(internalHolder.icon!!.icon.resId).isEqualTo(10)
+ }
+
+ @Test
+ fun externalSlot_alreadyEndsWithSuffix_suffixNotAddedTwice() {
+ underTest.setIcon("myslot$EXTERNAL_SLOT_SUFFIX", createExternalIcon())
+
+ assertThat(iconList.slots).hasSize(1)
+ assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX")
+ }
+
+ private fun createExternalIcon(): StatusBarIcon {
+ return StatusBarIcon(
+ "external.package",
+ UserHandle.ALL,
+ /* iconId= */ 2,
+ /* iconLevel= */ 0,
+ /* number= */ 0,
+ "contentDescription",
+ )
+ }
+}
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 14a319b..d8446f4 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
@@ -16,7 +16,8 @@
package com.android.systemui.statusbar.phone;
-import static com.android.systemui.flags.Flags.MODERN_BOUNCER;
+import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
+import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +38,8 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import android.window.WindowOnBackInvokedDispatcher;
@@ -54,9 +57,12 @@
import com.android.systemui.dock.DockManager;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.data.BouncerView;
import com.android.systemui.keyguard.data.BouncerViewDelegate;
+import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
@@ -102,10 +108,8 @@
@Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Mock private View mNotificationContainer;
@Mock private KeyguardBypassController mBypassController;
- @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
@Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
@Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
- @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
@Mock private KeyguardMessageArea mKeyguardMessageArea;
@Mock private ShadeController mShadeController;
@Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
@@ -115,18 +119,23 @@
@Mock private KeyguardSecurityModel mKeyguardSecurityModel;
@Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
@Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+ @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock private BouncerView mBouncerView;
@Mock private BouncerViewDelegate mBouncerViewDelegate;
+ @Mock private OnBackAnimationCallback mBouncerViewDelegateBackCallback;
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
- private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
+ private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
+ mBouncerExpansionCallback;
private FakeKeyguardStateController mKeyguardStateController =
spy(new FakeKeyguardStateController());
- @Mock private ViewRootImpl mViewRootImpl;
- @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
+ @Mock
+ private ViewRootImpl mViewRootImpl;
+ @Mock
+ private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
@Captor
- private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
+ private ArgumentCaptor<OnBackInvokedCallback> mBackCallbackCaptor;
@Before
@@ -137,8 +146,10 @@
when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
.thenReturn(mKeyguardMessageAreaController);
when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
-
- when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(true);
+ when(mBouncerViewDelegate.getBackCallback()).thenReturn(mBouncerViewDelegateBackCallback);
+ when(mFeatureFlags
+ .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM))
+ .thenReturn(true);
mStatusBarKeyguardViewManager =
new StatusBarKeyguardViewManager(
@@ -154,7 +165,6 @@
mock(NotificationShadeWindowController.class),
mKeyguardStateController,
mock(NotificationMediaManager.class),
- mKeyguardBouncerFactory,
mKeyguardMessageAreaFactory,
Optional.of(mSysUiUnfoldComponent),
() -> mShadeController,
@@ -163,7 +173,8 @@
mFeatureFlags,
mPrimaryBouncerCallbackInteractor,
mPrimaryBouncerInteractor,
- mBouncerView) {
+ mBouncerView,
+ mAlternateBouncerInteractor) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -179,8 +190,8 @@
mNotificationContainer,
mBypassController);
mStatusBarKeyguardViewManager.show(null);
- ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
- ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
+ ArgumentCaptor<PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
+ ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
callbackArgumentCaptor.capture());
mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
@@ -189,7 +200,8 @@
@Test
public void dismissWithAction_AfterKeyguardGoneSetToFalse() {
OnDismissAction action = () -> false;
- Runnable cancelAction = () -> {};
+ Runnable cancelAction = () -> {
+ };
mStatusBarKeyguardViewManager.dismissWithAction(
action, cancelAction, false /* afterKeyguardGone */);
verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction));
@@ -253,7 +265,7 @@
when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
+ verify(mPrimaryBouncerInteractor).setPanelExpansion(eq(EXPANSION_HIDDEN));
}
@Test
@@ -281,7 +293,7 @@
.thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(
expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* fraction= */ EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -298,7 +310,7 @@
.thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(
expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* fraction= */ EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -309,7 +321,7 @@
when(mKeyguardStateController.isOccluded()).thenReturn(true);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(
expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* fraction= */ EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -326,7 +338,7 @@
.thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(
expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* fraction= */ EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -337,7 +349,7 @@
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(
expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
+ /* fraction= */ EXPANSION_VISIBLE,
/* expanded= */ true,
/* tracking= */ false));
verify(mPrimaryBouncerInteractor, never()).setPanelExpansion(anyFloat());
@@ -434,37 +446,35 @@
@Test
public void testShowing_whenAlternateAuthShowing() {
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
- when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
assertTrue(
- "Is showing not accurate when alternative auth showing",
+ "Is showing not accurate when alternative bouncer is visible",
mStatusBarKeyguardViewManager.isBouncerShowing());
}
@Test
public void testWillBeShowing_whenAlternateAuthShowing() {
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
- when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
assertTrue(
- "Is or will be showing not accurate when alternative auth showing",
+ "Is or will be showing not accurate when alternate bouncer is visible",
mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
}
@Test
- public void testHideAlternateBouncer_onShowBouncer() {
- // GIVEN alt auth is showing
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ public void testHideAlternateBouncer_onShowPrimaryBouncer() {
+ reset(mAlternateBouncerInteractor);
+
+ // GIVEN alt bouncer is showing
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
- when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
- reset(mAlternateBouncer);
+ when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
// WHEN showBouncer is called
mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
// THEN alt bouncer should be hidden
- verify(mAlternateBouncer).hideAlternateBouncer();
+ verify(mAlternateBouncerInteractor).hide();
}
@Test
@@ -479,11 +489,9 @@
@Test
public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
- // GIVEN alt auth exists, unlocking with biometric isn't allowed
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ // GIVEN cannot use alternate bouncer
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
- when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
- .thenReturn(false);
+ when(mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()).thenReturn(false);
// WHEN showGenericBouncer is called
final boolean scrimmed = true;
@@ -491,21 +499,19 @@
// THEN regular bouncer is shown
verify(mPrimaryBouncerInteractor).show(eq(scrimmed));
- verify(mAlternateBouncer, never()).showAlternateBouncer();
}
@Test
public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
- // GIVEN alt auth exists, unlocking with biometric is allowed
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+ // GIVEN will show alternate bouncer
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false);
- when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+ when(mAlternateBouncerInteractor.show()).thenReturn(true);
// WHEN showGenericBouncer is called
mStatusBarKeyguardViewManager.showBouncer(true);
// THEN alt auth bouncer is shown
- verify(mAlternateBouncer).showAlternateBouncer();
+ verify(mAlternateBouncerInteractor).show();
verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
}
@@ -543,12 +549,12 @@
mBouncerExpansionCallback.onVisibilityChanged(true);
verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
- mOnBackInvokedCallback.capture());
+ mBackCallbackCaptor.capture());
/* verify that the same callback is unregistered when the bouncer becomes invisible */
mBouncerExpansionCallback.onVisibilityChanged(false);
verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(
- eq(mOnBackInvokedCallback.getValue()));
+ eq(mBackCallbackCaptor.getValue()));
}
@Test
@@ -557,18 +563,63 @@
/* capture the predictive back callback during registration */
verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
- mOnBackInvokedCallback.capture());
+ mBackCallbackCaptor.capture());
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
/* invoke the back callback directly */
- mOnBackInvokedCallback.getValue().onBackInvoked();
+ mBackCallbackCaptor.getValue().onBackInvoked();
/* verify that the bouncer will be hidden as a result of the invocation */
verify(mCentralSurfaces).setBouncerShowing(eq(false));
}
@Test
+ public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() {
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
+ .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
+ mBouncerExpansionCallback.onVisibilityChanged(true);
+ /* capture the predictive back callback during registration */
+ verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+ mBackCallbackCaptor.capture());
+ assertTrue(mBackCallbackCaptor.getValue() instanceof OnBackAnimationCallback);
+
+ OnBackAnimationCallback backCallback =
+ (OnBackAnimationCallback) mBackCallbackCaptor.getValue();
+
+ BackEvent event = new BackEvent(0, 0, 0, BackEvent.EDGE_LEFT);
+ backCallback.onBackStarted(event);
+ verify(mBouncerViewDelegateBackCallback, never()).onBackStarted(any());
+ }
+
+ @Test
+ public void testPredictiveBackCallback_forwardsBackDispatches() {
+ mBouncerExpansionCallback.onVisibilityChanged(true);
+ /* capture the predictive back callback during registration */
+ verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+ eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+ mBackCallbackCaptor.capture());
+ assertTrue(mBackCallbackCaptor.getValue() instanceof OnBackAnimationCallback);
+
+ OnBackAnimationCallback backCallback =
+ (OnBackAnimationCallback) mBackCallbackCaptor.getValue();
+
+ BackEvent event = new BackEvent(0, 0, 0, BackEvent.EDGE_LEFT);
+ backCallback.onBackStarted(event);
+ verify(mBouncerViewDelegateBackCallback).onBackStarted(eq(event));
+
+ backCallback.onBackProgressed(event);
+ verify(mBouncerViewDelegateBackCallback).onBackProgressed(eq(event));
+
+ backCallback.onBackInvoked();
+ verify(mBouncerViewDelegateBackCallback).onBackInvoked();
+
+ backCallback.onBackCancelled();
+ verify(mBouncerViewDelegateBackCallback).onBackCancelled();
+ }
+
+ @Test
public void testReportBouncerOnDreamWhenVisible() {
mBouncerExpansionCallback.onVisibilityChanged(true);
verify(mCentralSurfaces).setBouncerShowingOverDream(false);
@@ -604,7 +655,6 @@
mock(NotificationShadeWindowController.class),
mKeyguardStateController,
mock(NotificationMediaManager.class),
- mKeyguardBouncerFactory,
mKeyguardMessageAreaFactory,
Optional.of(mSysUiUnfoldComponent),
() -> mShadeController,
@@ -613,7 +663,8 @@
mFeatureFlags,
mPrimaryBouncerCallbackInteractor,
mPrimaryBouncerInteractor,
- mBouncerView) {
+ mBouncerView,
+ mAlternateBouncerInteractor) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java
deleted file mode 100644
index 96fba39..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java
+++ /dev/null
@@ -1,641 +0,0 @@
-/*
- * Copyright (C) 2018 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.statusbar.phone;
-
-import static com.android.systemui.flags.Flags.MODERN_BOUNCER;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewRootImpl;
-import android.window.OnBackInvokedCallback;
-import android.window.OnBackInvokedDispatcher;
-import android.window.WindowOnBackInvokedDispatcher;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.util.LatencyTracker;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.keyguard.KeyguardMessageArea;
-import com.android.keyguard.KeyguardMessageAreaController;
-import com.android.keyguard.KeyguardSecurityModel;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.ViewMediatorCallback;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dock.DockManager;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.keyguard.data.BouncerView;
-import com.android.systemui.keyguard.data.BouncerViewDelegate;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
-import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
-import com.android.systemui.navigationbar.NavigationModeController;
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.shade.NotificationPanelViewController;
-import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeExpansionChangeEvent;
-import com.android.systemui.shade.ShadeExpansionStateManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.unfold.SysUIUnfoldComponent;
-
-import com.google.common.truth.Truth;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-/**
- * StatusBarKeyguardViewManager Test with deprecated KeyguardBouncer.java.
- * TODO: Delete when deleting {@link KeyguardBouncer}
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase {
- private static final ShadeExpansionChangeEvent EXPANSION_EVENT =
- expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true);
-
- @Mock private ViewMediatorCallback mViewMediatorCallback;
- @Mock private LockPatternUtils mLockPatternUtils;
- @Mock private CentralSurfaces mCentralSurfaces;
- @Mock private ViewGroup mContainer;
- @Mock private NotificationPanelViewController mNotificationPanelView;
- @Mock private BiometricUnlockController mBiometricUnlockController;
- @Mock private SysuiStatusBarStateController mStatusBarStateController;
- @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- @Mock private View mNotificationContainer;
- @Mock private KeyguardBypassController mBypassController;
- @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
- @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
- @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
- @Mock private KeyguardBouncer mPrimaryBouncer;
- @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
- @Mock private KeyguardMessageArea mKeyguardMessageArea;
- @Mock private ShadeController mShadeController;
- @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
- @Mock private DreamOverlayStateController mDreamOverlayStateController;
- @Mock private LatencyTracker mLatencyTracker;
- @Mock private FeatureFlags mFeatureFlags;
- @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
- @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
- @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
- @Mock private BouncerView mBouncerView;
- @Mock private BouncerViewDelegate mBouncerViewDelegate;
-
- private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
- private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
- private FakeKeyguardStateController mKeyguardStateController =
- spy(new FakeKeyguardStateController());
-
- @Mock private ViewRootImpl mViewRootImpl;
- @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
- @Captor
- private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
-
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mKeyguardBouncerFactory.create(
- any(ViewGroup.class),
- any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class)))
- .thenReturn(mPrimaryBouncer);
- when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer);
- when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
- when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
- .thenReturn(mKeyguardMessageAreaController);
- when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
-
- mStatusBarKeyguardViewManager =
- new StatusBarKeyguardViewManager(
- getContext(),
- mViewMediatorCallback,
- mLockPatternUtils,
- mStatusBarStateController,
- mock(ConfigurationController.class),
- mKeyguardUpdateMonitor,
- mDreamOverlayStateController,
- mock(NavigationModeController.class),
- mock(DockManager.class),
- mock(NotificationShadeWindowController.class),
- mKeyguardStateController,
- mock(NotificationMediaManager.class),
- mKeyguardBouncerFactory,
- mKeyguardMessageAreaFactory,
- Optional.of(mSysUiUnfoldComponent),
- () -> mShadeController,
- mLatencyTracker,
- mKeyguardSecurityModel,
- mFeatureFlags,
- mPrimaryBouncerCallbackInteractor,
- mPrimaryBouncerInteractor,
- mBouncerView) {
- @Override
- public ViewRootImpl getViewRootImpl() {
- return mViewRootImpl;
- }
- };
- when(mViewRootImpl.getOnBackInvokedDispatcher())
- .thenReturn(mOnBackInvokedDispatcher);
- mStatusBarKeyguardViewManager.registerCentralSurfaces(
- mCentralSurfaces,
- mNotificationPanelView,
- new ShadeExpansionStateManager(),
- mBiometricUnlockController,
- mNotificationContainer,
- mBypassController);
- mStatusBarKeyguardViewManager.show(null);
- ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
- ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
- verify(mKeyguardBouncerFactory).create(any(ViewGroup.class),
- callbackArgumentCaptor.capture());
- mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
- }
-
- @Test
- public void dismissWithAction_AfterKeyguardGoneSetToFalse() {
- OnDismissAction action = () -> false;
- Runnable cancelAction = () -> {};
- mStatusBarKeyguardViewManager.dismissWithAction(
- action, cancelAction, false /* afterKeyguardGone */);
- verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction));
- }
-
- @Test
- public void showBouncer_onlyWhenShowing() {
- mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
- verify(mPrimaryBouncer, never()).show(anyBoolean());
- }
-
- @Test
- public void showBouncer_notWhenBouncerAlreadyShowing() {
- mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
- when(mPrimaryBouncer.isSecure()).thenReturn(true);
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
- verify(mPrimaryBouncer, never()).show(anyBoolean());
- }
-
- @Test
- public void showBouncer_showsTheBouncer() {
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
- verify(mPrimaryBouncer).show(anyBoolean(), eq(true));
- }
-
- @Test
- public void onPanelExpansionChanged_neverShowsDuringHintAnimation() {
- when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void onPanelExpansionChanged_propagatesToBouncerOnlyIfShowing() {
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
-
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(
- expansionEvent(/* fraction= */ 0.6f, /* expanded= */ false, /* tracking= */ true));
- verify(mPrimaryBouncer).setExpansion(eq(0.6f));
- }
-
- @Test
- public void onPanelExpansionChanged_duplicateEventsAreIgnored() {
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer).setExpansion(eq(0.5f));
-
- reset(mPrimaryBouncer);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
- }
-
- @Test
- public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() {
- mStatusBarKeyguardViewManager.hide(0, 0);
- when(mPrimaryBouncer.inTransit()).thenReturn(true);
-
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
- }
-
- @Test
- public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
- mKeyguardStateController.setCanDismissLockScreen(false);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer).show(eq(false), eq(false));
-
- // But not when it's already visible
- reset(mPrimaryBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
-
- // Or animating away
- reset(mPrimaryBouncer);
- when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
- }
-
- @Test
- public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() {
- when(mBiometricUnlockController.getMode())
- .thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(
- expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
- /* expanded= */ true,
- /* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void onPanelExpansionChanged_neverTranslatesBouncerWhenDismissBouncer() {
- // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing
- // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel
- // which would mistakenly cause the bouncer to show briefly before its visibility
- // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the
- // bouncer if the bouncer is dismissing as a result of a biometric unlock.
- when(mBiometricUnlockController.getMode())
- .thenReturn(BiometricUnlockController.MODE_DISMISS_BOUNCER);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(
- expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
- /* expanded= */ true,
- /* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() {
- when(mKeyguardStateController.isOccluded()).thenReturn(true);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(
- expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
- /* expanded= */ true,
- /* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void onPanelExpansionChanged_neverTranslatesBouncerWhenShowBouncer() {
- // Since KeyguardBouncer.EXPANSION_VISIBLE = 0 panel expansion, if the unlock is dismissing
- // the bouncer, there may be an onPanelExpansionChanged(0) call to collapse the panel
- // which would mistakenly cause the bouncer to show briefly before its visibility
- // is set to hide. Therefore, we don't want to propagate panelExpansionChanged to the
- // bouncer if the bouncer is dismissing as a result of a biometric unlock.
- when(mBiometricUnlockController.getMode())
- .thenReturn(BiometricUnlockController.MODE_SHOW_BOUNCER);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(
- expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
- /* expanded= */ true,
- /* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void onPanelExpansionChanged_neverTranslatesBouncerWhenShadeLocked() {
- when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(
- expansionEvent(
- /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
- /* expanded= */ true,
- /* tracking= */ false));
- verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
- }
-
- @Test
- public void setOccluded_animatesPanelExpansion_onlyIfBouncerHidden() {
- mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
- verify(mCentralSurfaces).animateKeyguardUnoccluding();
-
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
- clearInvocations(mCentralSurfaces);
- mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
- verify(mCentralSurfaces, never()).animateKeyguardUnoccluding();
- }
-
- @Test
- public void setOccluded_onKeyguardOccludedChangedCalled() {
- clearInvocations(mKeyguardStateController);
- clearInvocations(mKeyguardUpdateMonitor);
-
- mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
- verify(mKeyguardStateController).notifyKeyguardState(true, false);
-
- clearInvocations(mKeyguardUpdateMonitor);
- clearInvocations(mKeyguardStateController);
-
- mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
- verify(mKeyguardStateController).notifyKeyguardState(true, true);
-
- clearInvocations(mKeyguardUpdateMonitor);
- clearInvocations(mKeyguardStateController);
-
- mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */);
- verify(mKeyguardStateController).notifyKeyguardState(true, false);
- }
-
- @Test
- public void setOccluded_isInLaunchTransition_onKeyguardOccludedChangedCalled() {
- mStatusBarKeyguardViewManager.show(null);
-
- mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
- verify(mKeyguardStateController).notifyKeyguardState(true, true);
- }
-
- @Test
- public void setOccluded_isLaunchingActivityOverLockscreen_onKeyguardOccludedChangedCalled() {
- when(mCentralSurfaces.isLaunchingActivityOverLockscreen()).thenReturn(true);
- mStatusBarKeyguardViewManager.show(null);
-
- mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
- verify(mKeyguardStateController).notifyKeyguardState(true, true);
- }
-
- @Test
- public void testHiding_cancelsGoneRunnable() {
- OnDismissAction action = mock(OnDismissAction.class);
- Runnable cancelAction = mock(Runnable.class);
- mStatusBarKeyguardViewManager.dismissWithAction(
- action, cancelAction, true /* afterKeyguardGone */);
-
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- mStatusBarKeyguardViewManager.hideBouncer(true);
- mStatusBarKeyguardViewManager.hide(0, 30);
- verify(action, never()).onDismiss();
- verify(cancelAction).run();
- }
-
- @Test
- public void testHidingBouncer_cancelsGoneRunnable() {
- OnDismissAction action = mock(OnDismissAction.class);
- Runnable cancelAction = mock(Runnable.class);
- mStatusBarKeyguardViewManager.dismissWithAction(
- action, cancelAction, true /* afterKeyguardGone */);
-
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- mStatusBarKeyguardViewManager.hideBouncer(true);
-
- verify(action, never()).onDismiss();
- verify(cancelAction).run();
- }
-
- @Test
- public void testHiding_doesntCancelWhenShowing() {
- OnDismissAction action = mock(OnDismissAction.class);
- Runnable cancelAction = mock(Runnable.class);
- mStatusBarKeyguardViewManager.dismissWithAction(
- action, cancelAction, true /* afterKeyguardGone */);
-
- mStatusBarKeyguardViewManager.hide(0, 30);
- verify(action).onDismiss();
- verify(cancelAction, never()).run();
- }
-
- @Test
- public void testShowing_whenAlternateAuthShowing() {
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
- assertTrue(
- "Is showing not accurate when alternative auth showing",
- mStatusBarKeyguardViewManager.isBouncerShowing());
- }
-
- @Test
- public void testWillBeShowing_whenAlternateAuthShowing() {
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
- assertTrue(
- "Is or will be showing not accurate when alternative auth showing",
- mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
- }
-
- @Test
- public void testHideAlternateBouncer_onShowBouncer() {
- // GIVEN alt auth is showing
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
- reset(mAlternateBouncer);
-
- // WHEN showBouncer is called
- mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
-
- // THEN alt bouncer should be hidden
- verify(mAlternateBouncer).hideAlternateBouncer();
- }
-
- @Test
- public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() {
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mPrimaryBouncer.inTransit()).thenReturn(true);
-
- assertTrue(
- "Is or will be showing should be true when bouncer is in transit",
- mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
- }
-
- @Test
- public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
- // GIVEN alt auth exists, unlocking with biometric isn't allowed
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
- .thenReturn(false);
-
- // WHEN showGenericBouncer is called
- final boolean scrimmed = true;
- mStatusBarKeyguardViewManager.showBouncer(scrimmed);
-
- // THEN regular bouncer is shown
- verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed));
- verify(mAlternateBouncer, never()).showAlternateBouncer();
- }
-
- @Test
- public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
- // GIVEN alt auth exists, unlocking with biometric is allowed
- mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
- when(mPrimaryBouncer.isShowing()).thenReturn(false);
- when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
-
- // WHEN showGenericBouncer is called
- mStatusBarKeyguardViewManager.showBouncer(true);
-
- // THEN alt auth bouncer is shown
- verify(mAlternateBouncer).showAlternateBouncer();
- verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
- }
-
- @Test
- public void testUpdateResources_delegatesToBouncer() {
- mStatusBarKeyguardViewManager.updateResources();
-
- verify(mPrimaryBouncer).updateResources();
- }
-
- @Test
- public void updateKeyguardPosition_delegatesToBouncer() {
- mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f);
-
- verify(mPrimaryBouncer).updateKeyguardPosition(1.0f);
- }
-
- @Test
- public void testIsBouncerInTransit() {
- when(mPrimaryBouncer.inTransit()).thenReturn(true);
- Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue();
- when(mPrimaryBouncer.inTransit()).thenReturn(false);
- Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
- mPrimaryBouncer = null;
- Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
- }
-
- private static ShadeExpansionChangeEvent expansionEvent(
- float fraction, boolean expanded, boolean tracking) {
- return new ShadeExpansionChangeEvent(
- fraction, expanded, tracking, /* dragDownPxAmount= */ 0f);
- }
-
- @Test
- public void testPredictiveBackCallback_registration() {
- /* verify that a predictive back callback is registered when the bouncer becomes visible */
- mBouncerExpansionCallback.onVisibilityChanged(true);
- verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
- eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
- mOnBackInvokedCallback.capture());
-
- /* verify that the same callback is unregistered when the bouncer becomes invisible */
- mBouncerExpansionCallback.onVisibilityChanged(false);
- verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(
- eq(mOnBackInvokedCallback.getValue()));
- }
-
- @Test
- public void testPredictiveBackCallback_invocationHidesBouncer() {
- mBouncerExpansionCallback.onVisibilityChanged(true);
- /* capture the predictive back callback during registration */
- verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
- eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
- mOnBackInvokedCallback.capture());
-
- when(mPrimaryBouncer.isShowing()).thenReturn(true);
- when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
- /* invoke the back callback directly */
- mOnBackInvokedCallback.getValue().onBackInvoked();
-
- /* verify that the bouncer will be hidden as a result of the invocation */
- verify(mCentralSurfaces).setBouncerShowing(eq(false));
- }
-
- @Test
- public void testReportBouncerOnDreamWhenVisible() {
- mBouncerExpansionCallback.onVisibilityChanged(true);
- verify(mCentralSurfaces).setBouncerShowingOverDream(false);
- Mockito.clearInvocations(mCentralSurfaces);
- when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
- mBouncerExpansionCallback.onVisibilityChanged(true);
- verify(mCentralSurfaces).setBouncerShowingOverDream(true);
- }
-
- @Test
- public void testReportBouncerOnDreamWhenNotVisible() {
- mBouncerExpansionCallback.onVisibilityChanged(false);
- verify(mCentralSurfaces).setBouncerShowingOverDream(false);
- Mockito.clearInvocations(mCentralSurfaces);
- when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
- mBouncerExpansionCallback.onVisibilityChanged(false);
- verify(mCentralSurfaces).setBouncerShowingOverDream(false);
- }
-
- @Test
- public void flag_off_DoesNotCallBouncerInteractor() {
- when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false);
- mStatusBarKeyguardViewManager.hideBouncer(false);
- verify(mPrimaryBouncerInteractor, never()).hide();
- }
-
- @Test
- public void hideAlternateBouncer_beforeCentralSurfacesRegistered() {
- mStatusBarKeyguardViewManager =
- new StatusBarKeyguardViewManager(
- getContext(),
- mViewMediatorCallback,
- mLockPatternUtils,
- mStatusBarStateController,
- mock(ConfigurationController.class),
- mKeyguardUpdateMonitor,
- mDreamOverlayStateController,
- mock(NavigationModeController.class),
- mock(DockManager.class),
- mock(NotificationShadeWindowController.class),
- mKeyguardStateController,
- mock(NotificationMediaManager.class),
- mKeyguardBouncerFactory,
- mKeyguardMessageAreaFactory,
- Optional.of(mSysUiUnfoldComponent),
- () -> mShadeController,
- mLatencyTracker,
- mKeyguardSecurityModel,
- mFeatureFlags,
- mPrimaryBouncerCallbackInteractor,
- mPrimaryBouncerInteractor,
- mBouncerView) {
- @Override
- public ViewRootImpl getViewRootImpl() {
- return mViewRootImpl;
- }
- };
-
- // the following call before registering centralSurfaces should NOT throw a NPE:
- mStatusBarKeyguardViewManager.hideAlternateBouncer(true);
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 19658e6..ccc57ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -33,6 +33,7 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
@@ -59,6 +60,7 @@
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowViewController;
import com.android.systemui.shade.ShadeControllerImpl;
@@ -139,6 +141,8 @@
private ActivityLaunchAnimator mActivityLaunchAnimator;
@Mock
private InteractionJankMonitor mJankMonitor;
+ @Mock
+ private UserTracker mUserTracker;
private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mBubbleNotificationRow;
@@ -183,6 +187,8 @@
when(mVisibilityProvider.obtain(any(NotificationEntry.class), anyBoolean()))
.thenAnswer(invocation -> NotificationVisibility.obtain(
invocation.<NotificationEntry>getArgument(0).getKey(), 0, 1, false));
+ when(mUserTracker.getUserHandle()).thenReturn(
+ UserHandle.of(ActivityManager.getCurrentUser()));
HeadsUpManagerPhone headsUpManager = mock(HeadsUpManagerPhone.class);
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider =
@@ -222,7 +228,8 @@
mActivityLaunchAnimator,
notificationAnimationProvider,
mock(LaunchFullScreenIntentProvider.class),
- mock(FeatureFlags.class)
+ mock(FeatureFlags.class),
+ mUserTracker
);
// set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index eef43bd..8841521 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -40,6 +40,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.ShadeController;
@@ -92,7 +93,7 @@
mMetricsLogger = new FakeMetricsLogger();
LockscreenGestureLogger lockscreenGestureLogger = new LockscreenGestureLogger(
mMetricsLogger);
- mCommandQueue = new CommandQueue(mContext);
+ mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext));
mDependency.injectTestDependency(StatusBarStateController.class,
mock(SysuiStatusBarStateController.class));
mDependency.injectTestDependency(ShadeController.class, mShadeController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index 613238f..929099a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -32,6 +32,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.ActionClickLogger;
import com.android.systemui.statusbar.CommandQueue;
@@ -78,7 +79,8 @@
mRemoteInputCallback = spy(new StatusBarRemoteInputCallback(mContext,
mock(GroupExpansionManager.class), mNotificationLockscreenUserManager,
mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager,
- mActivityStarter, mShadeController, new CommandQueue(mContext),
+ mActivityStarter, mShadeController,
+ new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
mock(ActionClickLogger.class), mFakeExecutor));
mRemoteInputCallback.mChallengeReceiver = mRemoteInputCallback.new ChallengeReceiver();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 438271c..07e8d3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -23,10 +23,12 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -45,6 +47,7 @@
import androidx.test.filters.SmallTest;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.R;
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.dump.DumpManager;
@@ -58,7 +61,6 @@
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.DisableFlagsLogger;
import com.android.systemui.statusbar.OperatorNameViewController;
-import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
@@ -68,6 +70,8 @@
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
import com.android.systemui.util.CarrierConfigTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.settings.SecureSettings;
@@ -80,6 +84,9 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -92,7 +99,6 @@
private StatusBarLocationPublisher mLocationPublisher;
// Set in instantiate()
private StatusBarIconController mStatusBarIconController;
- private NetworkController mNetworkController;
private KeyguardStateController mKeyguardStateController;
private final CommandQueue mCommandQueue = mock(CommandQueue.class);
@@ -120,6 +126,12 @@
private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
@Mock
private DumpManager mDumpManager;
+ @Mock
+ private StatusBarWindowStateController mStatusBarWindowStateController;
+ @Mock
+ private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+ private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
public CollapsedStatusBarFragmentTest() {
super(CollapsedStatusBarFragment.class);
@@ -129,6 +141,14 @@
public void setup() {
injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
mDependency.injectMockDependency(DarkIconDispatcher.class);
+
+ // Keep the window state listeners so we can dispatch to them to test the status bar
+ // fragment's response.
+ doAnswer(invocation -> {
+ mStatusBarWindowStateListeners.add(invocation.getArgument(0));
+ return null;
+ }).when(mStatusBarWindowStateController).addListener(
+ any(StatusBarWindowStateListener.class));
}
@Test
@@ -416,6 +436,27 @@
assertFalse(contains);
}
+ @Test
+ public void testStatusBarIcons_hiddenThroughoutCameraLaunch() {
+ final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+
+ mockSecureCameraLaunch(fragment, true /* launched */);
+
+ // Status icons should be invisible or gone, but certainly not VISIBLE.
+ assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+ mockSecureCameraLaunchFinished();
+
+ assertNotEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertNotEquals(View.VISIBLE, getClockView().getVisibility());
+
+ mockSecureCameraLaunch(fragment, false /* launched */);
+
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
+ assertEquals(View.VISIBLE, getClockView().getVisibility());
+ }
+
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
@@ -424,7 +465,6 @@
mAnimationScheduler = mock(SystemStatusAnimationScheduler.class);
mLocationPublisher = mock(StatusBarLocationPublisher.class);
mStatusBarIconController = mock(StatusBarIconController.class);
- mNetworkController = mock(NetworkController.class);
mStatusBarStateController = mock(StatusBarStateController.class);
mKeyguardStateController = mock(KeyguardStateController.class);
mOperatorNameViewController = mock(OperatorNameViewController.class);
@@ -448,7 +488,6 @@
mStatusBarHideIconsForBouncerManager,
mKeyguardStateController,
mNotificationPanelViewController,
- mNetworkController,
mStatusBarStateController,
mCommandQueue,
mCarrierConfigTracker,
@@ -459,7 +498,9 @@
mOperatorNameViewControllerFactory,
mSecureSettings,
mExecutor,
- mDumpManager);
+ mDumpManager,
+ mStatusBarWindowStateController,
+ mKeyguardUpdateMonitor);
}
private void setUpDaggerComponent() {
@@ -482,6 +523,35 @@
mNotificationAreaInner);
}
+ /**
+ * Configure mocks to return values consistent with the secure camera animating itself launched
+ * over the keyguard.
+ */
+ private void mockSecureCameraLaunch(CollapsedStatusBarFragment fragment, boolean launched) {
+ when(mKeyguardUpdateMonitor.isSecureCameraLaunchedOverKeyguard()).thenReturn(launched);
+ when(mKeyguardStateController.isOccluded()).thenReturn(launched);
+
+ if (launched) {
+ fragment.onCameraLaunchGestureDetected(0 /* source */);
+ } else {
+ for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+ listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_SHOWING);
+ }
+ }
+
+ fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+ }
+
+ /**
+ * Configure mocks to return values consistent with the secure camera showing over the keyguard
+ * with its launch animation finished.
+ */
+ private void mockSecureCameraLaunchFinished() {
+ for (StatusBarWindowStateListener listener : mStatusBarWindowStateListeners) {
+ listener.onStatusBarWindowStateChanged(StatusBarManager.WINDOW_STATE_HIDDEN);
+ }
+ }
+
private CollapsedStatusBarFragment resumeAndGetFragment() {
mFragments.dispatchResume();
processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index d30222f..711e4ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -50,7 +50,9 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.*
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.eq
@@ -83,7 +85,8 @@
private lateinit var notifCollectionListener: NotifCollectionListener
@Mock private lateinit var mockOngoingCallFlags: OngoingCallFlags
- @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
+ @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler:
+ SwipeStatusBarAwayGestureHandler
@Mock private lateinit var mockOngoingCallListener: OngoingCallListener
@Mock private lateinit var mockActivityStarter: ActivityStarter
@Mock private lateinit var mockIActivityManager: IActivityManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 49d4bdc..0add905e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -26,6 +26,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository
@@ -56,6 +57,10 @@
private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
+ override val activeSubChangedInGroupEvent: MutableSharedFlow<Unit> = MutableSharedFlow()
+
+ private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+ override val defaultDataSubId = _defaultDataSubId
private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
override val defaultMobileNetworkConnectivity = _mobileConnectivity
@@ -81,6 +86,10 @@
_subscriptions.value = subs
}
+ fun setDefaultDataSubId(id: Int) {
+ _defaultDataSubId.value = id
+ }
+
fun setMobileConnectivity(model: MobileConnectivityModel) {
_mobileConnectivity.value = model
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 5d377a8..0859d14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -34,6 +34,8 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.mock
@@ -71,8 +73,10 @@
private lateinit var underTest: MobileRepositorySwitcher
private lateinit var realRepo: MobileConnectionsRepositoryImpl
private lateinit var demoRepo: DemoMobileConnectionsRepository
- private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var wifiDataSource: DemoModeWifiDataSource
private lateinit var logFactory: TableLogBufferFactory
+ private lateinit var wifiRepository: FakeWifiRepository
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@@ -96,10 +100,15 @@
// Never start in demo mode
whenever(demoModeController.isInDemoMode).thenReturn(false)
- mockDataSource =
+ mobileDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
}
+ wifiDataSource =
+ mock<DemoModeWifiDataSource>().also {
+ whenever(it.wifiEvents).thenReturn(MutableStateFlow(null))
+ }
+ wifiRepository = FakeWifiRepository()
realRepo =
MobileConnectionsRepositoryImpl(
@@ -113,12 +122,14 @@
context,
IMMEDIATE,
scope,
+ wifiRepository,
mock(),
)
demoRepo =
DemoMobileConnectionsRepository(
- dataSource = mockDataSource,
+ mobileDataSource = mobileDataSource,
+ wifiDataSource = wifiDataSource,
scope = scope,
context = context,
logFactory = logFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index 2102085..6989b514 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -29,6 +29,8 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
@@ -63,10 +65,12 @@
private val testScope = TestScope(testDispatcher)
private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+ private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
private lateinit var connectionsRepo: DemoMobileConnectionsRepository
private lateinit var underTest: DemoMobileConnectionRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var mockWifiDataSource: DemoModeWifiDataSource
@Before
fun setUp() {
@@ -75,10 +79,15 @@
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
}
+ mockWifiDataSource =
+ mock<DemoModeWifiDataSource>().also {
+ whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+ }
connectionsRepo =
DemoMobileConnectionsRepository(
- dataSource = mockDataSource,
+ mobileDataSource = mockDataSource,
+ wifiDataSource = mockWifiDataSource,
scope = testScope.backgroundScope,
context = context,
logFactory = logFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index cdbe75e..f12d113 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -32,6 +32,8 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
@@ -57,21 +59,28 @@
private val testScope = TestScope(testDispatcher)
private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
+ private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
private lateinit var underTest: DemoMobileConnectionsRepository
- private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var wifiDataSource: DemoModeWifiDataSource
@Before
fun setUp() {
// The data source only provides one API, so we can mock it with a flow here for convenience
- mockDataSource =
+ mobileDataSource =
mock<DemoModeMobileConnectionDataSource>().also {
whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
}
+ wifiDataSource =
+ mock<DemoModeWifiDataSource>().also {
+ whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
+ }
underTest =
DemoMobileConnectionsRepository(
- dataSource = mockDataSource,
+ mobileDataSource = mobileDataSource,
+ wifiDataSource = wifiDataSource,
scope = testScope.backgroundScope,
context = context,
logFactory = logFactory,
@@ -81,6 +90,14 @@
}
@Test
+ fun `connectivity - defaults to connected and validated`() =
+ testScope.runTest {
+ val connectivity = underTest.defaultMobileNetworkConnectivity.value
+ assertThat(connectivity.isConnected).isTrue()
+ assertThat(connectivity.isValidated).isTrue()
+ }
+
+ @Test
fun `network event - create new subscription`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -97,6 +114,22 @@
}
@Test
+ fun `wifi carrier merged event - create new subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEmpty()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+ job.cancel()
+ }
+
+ @Test
fun `network event - reuses subscription when same Id`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -119,6 +152,28 @@
}
@Test
+ fun `wifi carrier merged event - reuses subscription when same Id`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEmpty()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+ // Second network event comes in with the same subId, does not create a new subscription
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].subscriptionId).isEqualTo(5)
+
+ job.cancel()
+ }
+
+ @Test
fun `multiple subscriptions`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -133,6 +188,35 @@
}
@Test
+ fun `mobile subscription and carrier merged subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+ assertThat(latest).hasSize(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `multiple mobile subscriptions and carrier merged subscription`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3)
+
+ assertThat(latest).hasSize(3)
+
+ job.cancel()
+ }
+
+ @Test
fun `mobile disabled event - disables connection - subId specified - single conn`() =
testScope.runTest {
var latest: List<SubscriptionModel>? = null
@@ -194,6 +278,112 @@
job.cancel()
}
+ @Test
+ fun `wifi network updates to disabled - carrier merged connection removed`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+
+ fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled
+
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `wifi network updates to active - carrier merged connection removed`() =
+ testScope.runTest {
+ var latest: List<SubscriptionModel>? = null
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+
+ fakeWifiEventFlow.value =
+ FakeWifiEventModel.Wifi(
+ level = 1,
+ activity = 0,
+ ssid = null,
+ validated = true,
+ )
+
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile sub updates to carrier merged - only one connection`() =
+ testScope.runTest {
+ var latestSubsList: List<SubscriptionModel>? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { latestSubsList = it }
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2)
+ assertThat(latestSubsList).hasSize(1)
+
+ val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+ fakeWifiEventFlow.value = carrierMergedEvent
+ assertThat(latestSubsList).hasSize(1)
+ val connection = connections!!.find { it.subId == 3 }!!
+ assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `mobile sub updates to carrier merged then back - has old mobile data`() =
+ testScope.runTest {
+ var latestSubsList: List<SubscriptionModel>? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { latestSubsList = it }
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ val mobileEvent = validMobileEvent(subId = 3, level = 2)
+ fakeNetworkEventFlow.value = mobileEvent
+ assertThat(latestSubsList).hasSize(1)
+
+ val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+ fakeWifiEventFlow.value = carrierMergedEvent
+ assertThat(latestSubsList).hasSize(1)
+ var connection = connections!!.find { it.subId == 3 }!!
+ assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+ // WHEN the carrier merged is removed
+ fakeWifiEventFlow.value =
+ FakeWifiEventModel.Wifi(
+ level = 4,
+ activity = 0,
+ ssid = null,
+ validated = true,
+ )
+
+ // THEN the subId=3 connection goes back to the mobile information
+ connection = connections!!.find { it.subId == 3 }!!
+ assertConnection(connection, mobileEvent)
+
+ job.cancel()
+ }
+
/** Regression test for b/261706421 */
@Test
fun `multiple connections - remove all - does not throw`() =
@@ -289,6 +479,51 @@
job.cancel()
}
+ @Test
+ fun `demo connection - two connections - update carrier merged - no affect on first`() =
+ testScope.runTest {
+ var currentEvent1 = validMobileEvent(subId = 1)
+ var connection1: DemoMobileConnectionRepository? = null
+ var currentEvent2 = validCarrierMergedEvent(subId = 2)
+ var connection2: DemoMobileConnectionRepository? = null
+ var connections: List<DemoMobileConnectionRepository>? = null
+ val job =
+ underTest.subscriptions
+ .onEach { infos ->
+ connections =
+ infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
+ }
+ .launchIn(this)
+
+ fakeNetworkEventFlow.value = currentEvent1
+ fakeWifiEventFlow.value = currentEvent2
+ assertThat(connections).hasSize(2)
+ connections!!.forEach {
+ when (it.subId) {
+ 1 -> connection1 = it
+ 2 -> connection2 = it
+ else -> Assert.fail("Unexpected subscription")
+ }
+ }
+
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+ currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4)
+ fakeWifiEventFlow.value = currentEvent2
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ // and vice versa
+ currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+ fakeNetworkEventFlow.value = currentEvent1
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ job.cancel()
+ }
+
private fun assertConnection(
conn: DemoMobileConnectionRepository,
model: FakeNetworkEventModel
@@ -315,6 +550,21 @@
else -> {}
}
}
+
+ private fun assertCarrierMergedConnection(
+ conn: DemoMobileConnectionRepository,
+ model: FakeWifiEventModel.CarrierMerged,
+ ) {
+ val connectionInfo: MobileConnectionModel = conn.connectionInfo.value
+ assertThat(conn.subId).isEqualTo(model.subscriptionId)
+ assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
+ assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false)
+ assertThat(connectionInfo.isRoaming).isEqualTo(false)
+ assertThat(connectionInfo.isEmergencyOnly).isFalse()
+ assertThat(connectionInfo.isGsm).isFalse()
+ assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected)
+ }
}
/** Convenience to create a valid fake network event with minimal params */
@@ -339,3 +589,14 @@
roaming = roaming,
name = "demo name",
)
+
+fun validCarrierMergedEvent(
+ subId: Int = 1,
+ level: Int = 1,
+ numberOfLevels: Int = 4,
+): FakeWifiEventModel.CarrierMerged =
+ FakeWifiEventModel.CarrierMerged(
+ subscriptionId = subId,
+ level = level,
+ numberOfLevels = numberOfLevels,
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
new file mode 100644
index 0000000..ea90150
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class CarrierMergedConnectionRepositoryTest : SysuiTestCase() {
+
+ private lateinit var underTest: CarrierMergedConnectionRepository
+
+ private lateinit var wifiRepository: FakeWifiRepository
+ @Mock private lateinit var logger: TableLogBuffer
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ wifiRepository = FakeWifiRepository()
+
+ underTest =
+ CarrierMergedConnectionRepository(
+ SUB_ID,
+ logger,
+ NetworkNameModel.Default("name"),
+ testScope.backgroundScope,
+ wifiRepository,
+ )
+ }
+
+ @Test
+ fun connectionInfo_inactiveWifi_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_activeWifi_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1))
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setIsWifiEnabled(true)
+ wifiRepository.setIsWifiDefault(true)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 3,
+ )
+ )
+
+ val expected =
+ MobileConnectionModel(
+ primaryLevel = 3,
+ cdmaLevel = 3,
+ dataConnectionState = DataConnectionState.Connected,
+ dataActivityDirection =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ ),
+ resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
+ isRoaming = false,
+ isEmergencyOnly = false,
+ operatorAlphaShort = null,
+ isInService = true,
+ isGsm = false,
+ carrierNetworkChangeActive = false,
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID + 10,
+ level = 3,
+ )
+ )
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+ assertThat(latest!!.primaryLevel).isNotEqualTo(3)
+ assertThat(latest!!.resolvedNetworkType)
+ .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
+
+ job.cancel()
+ }
+
+ // This scenario likely isn't possible, but write a test for it anyway
+ @Test
+ fun connectionInfo_carrierMergedButNotEnabled_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 3,
+ )
+ )
+ wifiRepository.setIsWifiEnabled(false)
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ // This scenario likely isn't possible, but write a test for it anyway
+ @Test
+ fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() =
+ testScope.runTest {
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 3,
+ )
+ )
+ wifiRepository.setIsWifiDefault(false)
+
+ assertThat(latest).isEqualTo(MobileConnectionModel())
+
+ job.cancel()
+ }
+
+ @Test
+ fun numberOfLevels_comesFromCarrierMerged() =
+ testScope.runTest {
+ var latest: Int? = null
+ val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(
+ networkId = NET_ID,
+ subscriptionId = SUB_ID,
+ level = 1,
+ numberOfLevels = 6,
+ )
+ )
+
+ assertThat(latest).isEqualTo(6)
+
+ job.cancel()
+ }
+
+ @Test
+ fun dataEnabled_matchesWifiEnabled() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setIsWifiEnabled(true)
+ assertThat(latest).isTrue()
+
+ wifiRepository.setIsWifiEnabled(false)
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun cdmaRoaming_alwaysFalse() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ private companion object {
+ const val SUB_ID = 123
+ const val NET_ID = 456
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
new file mode 100644
index 0000000..c02a4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -0,0 +1,389 @@
+/*
+ * 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.statusbar.pipeline.mobile.data.repository.prod
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/**
+ * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the
+ * repository interface it's switching on. These tests just need to verify that the entire interface
+ * properly switches over when the value of `isCarrierMerged` changes.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class FullMobileConnectionRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: FullMobileConnectionRepository
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val mobileMappings = FakeMobileMappingsProxy()
+ private val tableLogBuffer = mock<TableLogBuffer>()
+ private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
+ private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
+
+ private lateinit var connectionsRepo: FakeMobileConnectionsRepository
+ private val globalMobileDataSettingChangedEvent: Flow<Unit>
+ get() = connectionsRepo.globalMobileDataSettingChangedEvent
+
+ private lateinit var mobileRepo: FakeMobileConnectionRepository
+ private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
+
+ @Before
+ fun setUp() {
+ connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer)
+
+ mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+ carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+
+ whenever(
+ mobileFactory.build(
+ eq(SUB_ID),
+ any(),
+ eq(DEFAULT_NAME),
+ eq(SEP),
+ eq(globalMobileDataSettingChangedEvent),
+ )
+ )
+ .thenReturn(mobileRepo)
+ whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME)))
+ .thenReturn(carrierMergedRepo)
+ }
+
+ @Test
+ fun startingIsCarrierMerged_usesCarrierMergedInitially() =
+ testScope.runTest {
+ val carrierMergedConnectionInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator",
+ )
+ carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo)
+
+ initializeRepo(startingIsCarrierMerged = true)
+
+ assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo)
+ assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo)
+ verify(mobileFactory, never())
+ .build(
+ SUB_ID,
+ tableLogBuffer,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent
+ )
+ }
+
+ @Test
+ fun startingNotCarrierMerged_usesTypicalInitially() =
+ testScope.runTest {
+ val mobileConnectionInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Operator",
+ )
+ mobileRepo.setConnectionInfo(mobileConnectionInfo)
+
+ initializeRepo(startingIsCarrierMerged = false)
+
+ assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
+ assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo)
+ verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME)
+ }
+
+ @Test
+ fun activeRepo_matchesIsCarrierMerged() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+ var latest: MobileConnectionRepository? = null
+ val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+ underTest.setIsCarrierMerged(true)
+
+ assertThat(latest).isEqualTo(carrierMergedRepo)
+
+ underTest.setIsCarrierMerged(false)
+
+ assertThat(latest).isEqualTo(mobileRepo)
+
+ underTest.setIsCarrierMerged(true)
+
+ assertThat(latest).isEqualTo(carrierMergedRepo)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_getsUpdatesFromRepo_carrierMerged() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ underTest.setIsCarrierMerged(true)
+
+ val info1 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator",
+ primaryLevel = 1,
+ )
+ carrierMergedRepo.setConnectionInfo(info1)
+
+ assertThat(latest).isEqualTo(info1)
+
+ val info2 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator #2",
+ primaryLevel = 2,
+ )
+ carrierMergedRepo.setConnectionInfo(info2)
+
+ assertThat(latest).isEqualTo(info2)
+
+ val info3 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator #3",
+ primaryLevel = 3,
+ )
+ carrierMergedRepo.setConnectionInfo(info3)
+
+ assertThat(latest).isEqualTo(info3)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_getsUpdatesFromRepo_mobile() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ underTest.setIsCarrierMerged(false)
+
+ val info1 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Merged Operator",
+ primaryLevel = 1,
+ )
+ mobileRepo.setConnectionInfo(info1)
+
+ assertThat(latest).isEqualTo(info1)
+
+ val info2 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Merged Operator #2",
+ primaryLevel = 2,
+ )
+ mobileRepo.setConnectionInfo(info2)
+
+ assertThat(latest).isEqualTo(info2)
+
+ val info3 =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Merged Operator #3",
+ primaryLevel = 3,
+ )
+ mobileRepo.setConnectionInfo(info3)
+
+ assertThat(latest).isEqualTo(info3)
+
+ job.cancel()
+ }
+
+ @Test
+ fun connectionInfo_updatesWhenCarrierMergedUpdates() =
+ testScope.runTest {
+ initializeRepo(startingIsCarrierMerged = false)
+
+ var latest: MobileConnectionModel? = null
+ val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+ val carrierMergedInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Carrier Merged Operator",
+ primaryLevel = 4,
+ )
+ carrierMergedRepo.setConnectionInfo(carrierMergedInfo)
+
+ val mobileInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "Typical Operator",
+ primaryLevel = 2,
+ )
+ mobileRepo.setConnectionInfo(mobileInfo)
+
+ // Start with the mobile info
+ assertThat(latest).isEqualTo(mobileInfo)
+
+ // WHEN isCarrierMerged is set to true
+ underTest.setIsCarrierMerged(true)
+
+ // THEN the carrier merged info is used
+ assertThat(latest).isEqualTo(carrierMergedInfo)
+
+ val newCarrierMergedInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "New CM Operator",
+ primaryLevel = 0,
+ )
+ carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo)
+
+ assertThat(latest).isEqualTo(newCarrierMergedInfo)
+
+ // WHEN isCarrierMerged is set to false
+ underTest.setIsCarrierMerged(false)
+
+ // THEN the typical info is used
+ assertThat(latest).isEqualTo(mobileInfo)
+
+ val newMobileInfo =
+ MobileConnectionModel(
+ operatorAlphaShort = "New Mobile Operator",
+ primaryLevel = 3,
+ )
+ mobileRepo.setConnectionInfo(newMobileInfo)
+
+ assertThat(latest).isEqualTo(newMobileInfo)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `factory - reuses log buffers for same connection`() =
+ testScope.runTest {
+ val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+ val factory =
+ FullMobileConnectionRepository.Factory(
+ scope = testScope.backgroundScope,
+ realLoggerFactory,
+ mobileFactory,
+ carrierMergedFactory,
+ )
+
+ // Create two connections for the same subId. Similar to if the connection appeared
+ // and disappeared from the connectionFactory's perspective
+ val connection1 =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = false,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ val connection1Repeat =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = false,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ assertThat(connection1.tableLogBuffer)
+ .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+ }
+
+ @Test
+ fun `factory - reuses log buffers for same sub ID even if carrier merged`() =
+ testScope.runTest {
+ val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
+ val factory =
+ FullMobileConnectionRepository.Factory(
+ scope = testScope.backgroundScope,
+ realLoggerFactory,
+ mobileFactory,
+ carrierMergedFactory,
+ )
+
+ val connection1 =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = false,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ // WHEN a connection with the same sub ID but carrierMerged = true is created
+ val connection1Repeat =
+ factory.build(
+ SUB_ID,
+ startingIsCarrierMerged = true,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ )
+
+ // THEN the same table is re-used
+ assertThat(connection1.tableLogBuffer)
+ .isSameInstanceAs(connection1Repeat.tableLogBuffer)
+ }
+
+ // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo
+ // implements logging).
+
+ private fun initializeRepo(startingIsCarrierMerged: Boolean) {
+ underTest =
+ FullMobileConnectionRepository(
+ SUB_ID,
+ startingIsCarrierMerged,
+ tableLogBuffer,
+ DEFAULT_NAME,
+ SEP,
+ globalMobileDataSettingChangedEvent,
+ testScope.backgroundScope,
+ mobileFactory,
+ carrierMergedFactory,
+ )
+ }
+
+ private companion object {
+ const val SUB_ID = 42
+ private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+ private const val SEP = "-"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 0da15e2..db8172a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -22,6 +22,7 @@
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.os.ParcelUuid
import android.provider.Settings
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
@@ -38,8 +39,11 @@
import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
@@ -47,6 +51,7 @@
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
+import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -72,6 +77,9 @@
private lateinit var underTest: MobileConnectionsRepositoryImpl
private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
+ private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory
+ private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory
+ private lateinit var wifiRepository: FakeWifiRepository
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@@ -94,10 +102,23 @@
}
}
- whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ ->
+ whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ ->
mock<TableLogBuffer>()
}
+ // For convenience, set up the subscription info callbacks
+ whenever(subscriptionManager.getActiveSubscriptionInfo(anyInt())).thenAnswer { invocation ->
+ when (invocation.getArgument(0) as Int) {
+ 1 -> SUB_1
+ 2 -> SUB_2
+ 3 -> SUB_3
+ 4 -> SUB_4
+ else -> null
+ }
+ }
+
+ wifiRepository = FakeWifiRepository()
+
connectionFactory =
MobileConnectionRepositoryImpl.Factory(
fakeBroadcastDispatcher,
@@ -108,7 +129,18 @@
logger = logger,
mobileMappingsProxy = mobileMappings,
scope = scope,
+ )
+ carrierMergedFactory =
+ CarrierMergedConnectionRepository.Factory(
+ scope,
+ wifiRepository,
+ )
+ fullConnectionFactory =
+ FullMobileConnectionRepository.Factory(
+ scope = scope,
logFactory = logBufferFactory,
+ mobileRepoFactory = connectionFactory,
+ carrierMergedRepoFactory = carrierMergedFactory,
)
underTest =
@@ -123,7 +155,8 @@
context,
IMMEDIATE,
scope,
- connectionFactory,
+ wifiRepository,
+ fullConnectionFactory,
)
}
@@ -178,6 +211,40 @@
}
@Test
+ fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
+ runBlocking(IMMEDIATE) {
+ var latest: List<SubscriptionModel>? = null
+
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_CM))
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() =
+ runBlocking(IMMEDIATE) {
+ var latest: List<SubscriptionModel>? = null
+
+ val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM))
+
+ job.cancel()
+ }
+
+ @Test
fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
runBlocking(IMMEDIATE) {
assertThat(underTest.activeMobileDataSubscriptionId.value)
@@ -217,6 +284,96 @@
}
@Test
+ fun testConnectionRepository_carrierMergedSubId_isCached() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val repo1 = underTest.getRepoForSubId(SUB_CM_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_CM_ID)
+
+ assertThat(repo1).isSameInstanceAs(repo2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ val mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ // WHEN the wifi network updates to be not carrier merged
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1))
+
+ // THEN the repos update
+ val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ // WHEN the wifi network updates to be carrier merged
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+
+ // THEN the repos update
+ val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
+ mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
+ assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
+ assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
fun testConnectionCache_clearsInvalidSubscriptions() =
runBlocking(IMMEDIATE) {
val job = underTest.subscriptions.launchIn(this)
@@ -242,6 +399,34 @@
job.cancel()
}
+ @Test
+ fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ wifiRepository.setWifiNetwork(WIFI_NETWORK_CM)
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // Get repos to trigger caching
+ val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+ val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+ val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID)
+
+ assertThat(underTest.getSubIdRepoCache())
+ .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged)
+
+ // SUB_2 and SUB_CM disappear
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+ job.cancel()
+ }
+
/** Regression test for b/261706421 */
@Test
fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() =
@@ -292,14 +477,14 @@
// Get repos to trigger creation
underTest.getRepoForSubId(SUB_1_ID)
verify(logBufferFactory)
- .create(
- eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)),
+ .getOrCreate(
+ eq(tableBufferLogName(SUB_1_ID)),
anyInt(),
)
underTest.getRepoForSubId(SUB_2_ID)
verify(logBufferFactory)
- .create(
- eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)),
+ .getOrCreate(
+ eq(tableBufferLogName(SUB_2_ID)),
anyInt(),
)
@@ -307,6 +492,35 @@
}
@Test
+ fun testDefaultDataSubId_updatesOnBroadcast() =
+ runBlocking(IMMEDIATE) {
+ var latest: Int? = null
+ val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(
+ context,
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
+ )
+ }
+
+ assertThat(latest).isEqualTo(SUB_2_ID)
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(
+ context,
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+ )
+ }
+
+ assertThat(latest).isEqualTo(SUB_1_ID)
+
+ job.cancel()
+ }
+
+ @Test
fun mobileConnectivity_default() {
assertThat(underTest.defaultMobileNetworkConnectivity.value)
.isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
@@ -419,7 +633,8 @@
context,
IMMEDIATE,
scope,
- connectionFactory,
+ wifiRepository,
+ fullConnectionFactory,
)
var latest: MobileMappings.Config? = null
@@ -484,6 +699,38 @@
job.cancel()
}
+ @Test
+ fun `active data change - in same group - emits unit`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Unit? = null
+ val job = underTest.activeSubChangedInGroupEvent.onEach { latest = it }.launchIn(this)
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+ .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+ .onActiveDataSubscriptionIdChanged(SUB_4_ID_GROUPED)
+
+ assertThat(latest).isEqualTo(Unit)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `active data change - not in same group - does not emit`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Unit? = null
+ val job = underTest.activeSubChangedInGroupEvent.onEach { latest = it }.launchIn(this)
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+ .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+ .onActiveDataSubscriptionIdChanged(SUB_1_ID)
+
+ assertThat(latest).isEqualTo(null)
+
+ job.cancel()
+ }
+
private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected)
@@ -517,17 +764,59 @@
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
+
+ // Subscription 1
private const val SUB_1_ID = 1
private val SUB_1 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+ mock<SubscriptionInfo>().also {
+ whenever(it.subscriptionId).thenReturn(SUB_1_ID)
+ whenever(it.groupUuid).thenReturn(ParcelUuid(UUID.randomUUID()))
+ }
private val MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
+ // Subscription 2
private const val SUB_2_ID = 2
private val SUB_2 =
- mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+ mock<SubscriptionInfo>().also {
+ whenever(it.subscriptionId).thenReturn(SUB_2_ID)
+ whenever(it.groupUuid).thenReturn(ParcelUuid(UUID.randomUUID()))
+ }
private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
+ // Subs 3 and 4 are considered to be in the same group ------------------------------------
+ private val GROUP_ID_3_4 = ParcelUuid(UUID.randomUUID())
+
+ // Subscription 3
+ private const val SUB_3_ID_GROUPED = 3
+ private val SUB_3 =
+ mock<SubscriptionInfo>().also {
+ whenever(it.subscriptionId).thenReturn(SUB_3_ID_GROUPED)
+ whenever(it.groupUuid).thenReturn(GROUP_ID_3_4)
+ }
+
+ // Subscription 4
+ private const val SUB_4_ID_GROUPED = 4
+ private val SUB_4 =
+ mock<SubscriptionInfo>().also {
+ whenever(it.subscriptionId).thenReturn(SUB_4_ID_GROUPED)
+ whenever(it.groupUuid).thenReturn(GROUP_ID_3_4)
+ }
+
+ // Subs 3 and 4 are considered to be in the same group ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
private const val NET_ID = 123
private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
+
+ // Carrier merged subscription
+ private const val SUB_CM_ID = 5
+ private val SUB_CM =
+ mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) }
+ private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID)
+ private val WIFI_NETWORK_CM =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 3,
+ subscriptionId = SUB_CM_ID,
+ level = 1,
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index a29146b..7aeaa48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -40,6 +40,8 @@
)
)
+ override val isConnected = MutableStateFlow(true)
+
private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
override val networkTypeIconGroup = _iconGroup
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 1c00646..172755c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -23,6 +23,7 @@
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
@@ -59,6 +60,9 @@
override val alwaysShowDataRatIcon = MutableStateFlow(false)
override val alwaysUseCdmaLevel = MutableStateFlow(false)
+ override val defaultDataSubId = MutableStateFlow(DEFAULT_DATA_SUB_ID)
+
+ override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
override val defaultMobileIconMapping = _defaultMobileIconMapping
@@ -77,6 +81,8 @@
companion object {
val DEFAULT_ICON = TelephonyIcons.G
+ const val DEFAULT_DATA_SUB_ID = 1
+
// Use [MobileMappings] to define some simple definitions
const val THREE_G = NETWORK_TYPE_GSM
const val LTE = NETWORK_TYPE_LTE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 61e13b8..c42aba5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -60,8 +61,10 @@
mobileIconsInteractor.activeDataConnectionHasDataEnabled,
mobileIconsInteractor.alwaysShowDataRatIcon,
mobileIconsInteractor.alwaysUseCdmaLevel,
+ mobileIconsInteractor.defaultMobileNetworkConnectivity,
mobileIconsInteractor.defaultMobileIconMapping,
mobileIconsInteractor.defaultMobileIconGroup,
+ mobileIconsInteractor.defaultDataSubId,
mobileIconsInteractor.isDefaultConnectionFailed,
connectionRepository,
)
@@ -271,6 +274,47 @@
}
@Test
+ fun iconGroup_carrierMerged_usesOverride() =
+ runBlocking(IMMEDIATE) {
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType = CarrierMergedNetworkType,
+ ),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `icon group - checks default data`() =
+ runBlocking(IMMEDIATE) {
+ mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ ),
+ )
+
+ var latest: MobileIconGroup? = null
+ val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
+
+ // Default data sub id changes to something else
+ mobileIconsInteractor.defaultDataSubId.value = 123
+ yield()
+
+ assertThat(latest).isEqualTo(TelephonyIcons.NOT_DEFAULT_DATA)
+
+ job.cancel()
+ }
+
+ @Test
fun alwaysShowDataRatIcon_matchesParent() =
runBlocking(IMMEDIATE) {
var latest: Boolean? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index b82a584..bd24922 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -28,14 +28,17 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before
@@ -43,13 +46,16 @@
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
private lateinit var connectionsRepository: FakeMobileConnectionsRepository
private val userSetupRepository = FakeUserSetupRepository()
private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val scope = CoroutineScope(IMMEDIATE)
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
@Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -72,8 +78,9 @@
MobileIconsInteractorImpl(
connectionsRepository,
carrierConfigTracker,
+ logger = mock(),
userSetupRepository,
- scope
+ testScope.backgroundScope,
)
}
@@ -81,7 +88,7 @@
@Test
fun filteredSubscriptions_default() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
@@ -92,7 +99,7 @@
@Test
fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
var latest: List<SubscriptionModel>? = null
@@ -105,7 +112,7 @@
@Test
fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -122,7 +129,7 @@
@Test
fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -139,7 +146,7 @@
@Test
fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -157,7 +164,7 @@
@Test
fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -175,7 +182,7 @@
@Test
fun activeDataConnection_turnedOn() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
CONNECTION_1.setDataEnabled(true)
var latest: Boolean? = null
val job =
@@ -188,7 +195,7 @@
@Test
fun activeDataConnection_turnedOff() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
CONNECTION_1.setDataEnabled(true)
var latest: Boolean? = null
val job =
@@ -204,7 +211,7 @@
@Test
fun activeDataConnection_invalidSubId() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job =
underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
@@ -220,7 +227,7 @@
@Test
fun failedConnection_connected_validated_notFailed() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true))
@@ -233,7 +240,7 @@
@Test
fun failedConnection_notConnected_notValidated_notFailed() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
@@ -247,7 +254,7 @@
@Test
fun failedConnection_connected_notValidated_failed() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
@@ -261,7 +268,7 @@
@Test
fun alwaysShowDataRatIcon_configHasTrue() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
@@ -277,7 +284,7 @@
@Test
fun alwaysShowDataRatIcon_configHasFalse() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
@@ -293,7 +300,7 @@
@Test
fun alwaysUseCdmaLevel_configHasTrue() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
@@ -309,7 +316,7 @@
@Test
fun alwaysUseCdmaLevel_configHasFalse() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
@@ -323,8 +330,286 @@
job.cancel()
}
+ @Test
+ fun `default mobile connectivity - uses repo value`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ var expected = MobileConnectivityModel(isConnected = true, isValidated = true)
+ connectionsRepository.setMobileConnectivity(expected)
+ assertThat(latest).isEqualTo(expected)
+
+ expected = MobileConnectivityModel(isConnected = false, isValidated = true)
+ connectionsRepository.setMobileConnectivity(expected)
+ assertThat(latest).isEqualTo(expected)
+
+ expected = MobileConnectivityModel(isConnected = true, isValidated = false)
+ connectionsRepository.setMobileConnectivity(expected)
+ assertThat(latest).isEqualTo(expected)
+
+ expected = MobileConnectivityModel(isConnected = false, isValidated = false)
+ connectionsRepository.setMobileConnectivity(expected)
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data switch - in same group - validated matches previous value`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = true,
+ isValidated = true,
+ )
+ )
+ // Trigger a data change in the same subscription group
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = true,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data switch - in same group - validated matches previous value - expires after 2s`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = true,
+ isValidated = true,
+ )
+ )
+ // Trigger a data change in the same subscription group
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+ // After 1s, the force validation bit is still present
+ advanceTimeBy(1000)
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = true,
+ )
+ )
+
+ // After 2s, the force validation expires
+ advanceTimeBy(1001)
+
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data switch - in same group - not validated - uses new value immediately`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = true,
+ isValidated = false,
+ )
+ )
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data switch - lose validation - then switch happens - clears forced bit`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ // GIVEN the network starts validated
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = true,
+ isValidated = true,
+ )
+ )
+
+ // WHEN a data change happens in the same group
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+
+ // WHEN the validation bit is lost
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ // WHEN another data change happens in the same group
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+
+ // THEN the forced validation bit is still removed after 2s
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = true,
+ )
+ )
+
+ advanceTimeBy(1000)
+
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = true,
+ )
+ )
+
+ advanceTimeBy(1001)
+
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data switch - while already forcing validation - resets clock`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = true,
+ isValidated = true,
+ )
+ )
+
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+
+ advanceTimeBy(1000)
+
+ // WHEN another change in same group event happens
+ connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ // THEN the forced validation remains for exactly 2 more seconds from now
+
+ // 1.500s from second event
+ advanceTimeBy(1500)
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = true,
+ )
+ )
+
+ // 2.001s from the second event
+ advanceTimeBy(501)
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data switch - not in same group - uses new values`() =
+ testScope.runTest {
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = true,
+ isValidated = true,
+ )
+ )
+ connectionsRepository.setMobileConnectivity(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ assertThat(latest)
+ .isEqualTo(
+ MobileConnectivityModel(
+ isConnected = false,
+ isValidated = false,
+ )
+ )
+
+ job.cancel()
+ }
+
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
private val tableLogBuffer =
TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 2a8d42f..a24e29ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -274,6 +274,41 @@
}
@Test
+ fun `network type - alwaysShow - shown when not connected`() =
+ testScope.runTest {
+ interactor.setIconGroup(THREE_G)
+ interactor.isConnected.value = false
+ interactor.alwaysShowDataRatIcon.value = true
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription)
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network type - not shown when not connected`() =
+ testScope.runTest {
+ interactor.setIconGroup(THREE_G)
+ interactor.isDataConnected.value = true
+ interactor.isConnected.value = false
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
fun roaming() =
testScope.runTest {
interactor.isRoaming.value = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index b32058f..3dccbbf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -45,7 +45,7 @@
@Test
fun testLogNetworkCapsChange_bufferHasInfo() {
- logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS)
+ logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS, isDefaultNetworkCallback = true)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
@@ -54,6 +54,7 @@
val expectedNetId = NET_1_ID.toString()
val expectedCaps = NET_1_CAPS.toString()
+ assertThat(actualString).contains("true")
assertThat(actualString).contains(expectedNetId)
assertThat(actualString).contains(expectedCaps)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
index 3fe6983..e4c8fd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.shared.ui.view
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
@@ -118,6 +119,22 @@
assertThat(view.isIconVisible).isEqualTo(false)
}
+ @Test
+ fun getDrawingRect_takesTranslationIntoAccount() {
+ val view = createAndInitView()
+
+ view.translationX = 50f
+ view.translationY = 60f
+
+ val drawingRect = Rect()
+ view.getDrawingRect(drawingRect)
+
+ assertThat(drawingRect.left).isEqualTo(view.left + 50)
+ assertThat(drawingRect.right).isEqualTo(view.right + 50)
+ assertThat(drawingRect.top).isEqualTo(view.top + 60)
+ assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60)
+ }
+
private fun createAndInitView(): ModernStatusBarView {
val view = ModernStatusBarView(context, null)
binding = TestBinding()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
index 30ac8d4..824cebd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -16,11 +16,12 @@
package com.android.systemui.statusbar.pipeline.wifi.data.model
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
-import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -44,9 +45,53 @@
WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
}
+ @Test(expected = IllegalArgumentException::class)
+ fun carrierMerged_invalidSubId_exceptionThrown() {
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1)
+ }
+
// Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
@Test
+ fun logDiffs_carrierMergedToInactive_resetsAllFields() {
+ val logger = TestLogger()
+ val prevVal =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 5,
+ subscriptionId = 3,
+ level = 1,
+ )
+
+ WifiNetworkModel.Inactive.logDiffs(prevVal, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+ }
+
+ @Test
+ fun logDiffs_inactiveToCarrierMerged_logsAllFields() {
+ val logger = TestLogger()
+ val carrierMerged =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 6,
+ subscriptionId = 3,
+ level = 2,
+ )
+
+ carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+ assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+ }
+
+ @Test
fun logDiffs_inactiveToActive_logsAllActiveFields() {
val logger = TestLogger()
val activeNetwork =
@@ -95,8 +140,14 @@
level = 3,
ssid = "Test SSID"
)
+ val prevVal =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 5,
+ subscriptionId = 3,
+ level = 1,
+ )
- activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger)
+ activeNetwork.logDiffs(prevVal, logger)
assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
@@ -105,7 +156,7 @@
assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
}
@Test
- fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() {
+ fun logDiffs_activeToCarrierMerged_logsAllFields() {
val logger = TestLogger()
val activeNetwork =
WifiNetworkModel.Active(
@@ -114,13 +165,20 @@
level = 3,
ssid = "Test SSID"
)
+ val carrierMerged =
+ WifiNetworkModel.CarrierMerged(
+ networkId = 6,
+ subscriptionId = 3,
+ level = 2,
+ )
- WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger)
+ carrierMerged.logDiffs(prevVal = activeNetwork, logger)
assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
- assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
- assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
- assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6"))
+ assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, "2"))
assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 8f07615..7099f1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -21,11 +21,15 @@
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.TransportInfo
+import android.net.VpnTransportInfo
import android.net.vcn.VcnTransportInfo
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -242,6 +246,54 @@
job.cancel()
}
+ /** Regression test for b/266628069. */
+ @Test
+ fun isWifiDefault_transportInfoIsNotWifi_andNoWifiTransport_false() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.isWifiDefault.launchIn(this)
+
+ val transportInfo = VpnTransportInfo(
+ /* type= */ 0,
+ /* sessionId= */ "sessionId",
+ )
+ val networkCapabilities = mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_VPN)).thenReturn(true)
+ whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false)
+ whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
+ whenever(it.transportInfo).thenReturn(transportInfo)
+ }
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities)
+
+ assertThat(underTest.isWifiDefault.value).isFalse()
+
+ job.cancel()
+ }
+
+ /** Regression test for b/266628069. */
+ @Test
+ fun isWifiDefault_transportInfoIsNotWifi_butHasWifiTransport_true() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.isWifiDefault.launchIn(this)
+
+ val transportInfo = VpnTransportInfo(
+ /* type= */ 0,
+ /* sessionId= */ "sessionId",
+ )
+ val networkCapabilities = mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_VPN)).thenReturn(true)
+ whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
+ whenever(it.transportInfo).thenReturn(transportInfo)
+ }
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities)
+
+ assertThat(underTest.isWifiDefault.value).isTrue()
+
+ job.cancel()
+ }
+
@Test
fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) {
val job = underTest.isWifiDefault.launchIn(this)
@@ -259,6 +311,24 @@
}
@Test
+ fun wifiNetwork_cellularAndWifiTransports_usesCellular_isTrue() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.isWifiDefault.launchIn(this)
+
+ val capabilities = mock<NetworkCapabilities>().apply {
+ whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+ }
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+ assertThat(underTest.isWifiDefault.value).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) {
val job = underTest.isWifiDefault.launchIn(this)
@@ -340,7 +410,6 @@
.launchIn(this)
val wifiInfo = mock<WifiInfo>().apply {
- whenever(this.ssid).thenReturn(SSID)
whenever(this.isPrimary).thenReturn(true)
whenever(this.isCarrierMerged).thenReturn(true)
}
@@ -353,6 +422,67 @@
}
@Test
+ fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() =
+ runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.isPrimary).thenReturn(true)
+ whenever(this.isCarrierMerged).thenReturn(true)
+ whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID)
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(
+ NETWORK,
+ createWifiNetworkCapabilities(wifiInfo),
+ )
+
+ assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+
+ job.cancel()
+ }
+
+ @Test
+ fun wifiNetwork_isCarrierMerged_getsCorrectValues() =
+ runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val rssi = -57
+ val wifiInfo = mock<WifiInfo>().apply {
+ whenever(this.isPrimary).thenReturn(true)
+ whenever(this.isCarrierMerged).thenReturn(true)
+ whenever(this.rssi).thenReturn(rssi)
+ whenever(this.subscriptionId).thenReturn(567)
+ }
+
+ whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2)
+ whenever(wifiManager.maxSignalLevel).thenReturn(5)
+
+ getNetworkCallback().onCapabilitiesChanged(
+ NETWORK,
+ createWifiNetworkCapabilities(wifiInfo),
+ )
+
+ assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+ val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged
+ assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567)
+ assertThat(latestCarrierMerged.level).isEqualTo(2)
+ // numberOfLevels = maxSignalLevel + 1
+ assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6)
+
+ job.cancel()
+ }
+
+ @Test
fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) {
var latest: WifiNetworkModel? = null
val job = underTest
@@ -406,6 +536,28 @@
job.cancel()
}
+ /** Regression test for b/266628069. */
+ @Test
+ fun wifiNetwork_transportInfoIsNotWifi_flowHasNoNetwork() =
+ runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val transportInfo = VpnTransportInfo(
+ /* type= */ 0,
+ /* sessionId= */ "sessionId",
+ )
+ getNetworkCallback()
+ .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(transportInfo))
+
+ assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+ job.cancel()
+ }
+
@Test
fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = runBlocking(IMMEDIATE) {
var latest: WifiNetworkModel? = null
@@ -474,6 +626,31 @@
}
@Test
+ fun wifiNetwork_cellularAndWifiTransports_usesCellular() =
+ runBlocking(IMMEDIATE) {
+ var latest: WifiNetworkModel? = null
+ val job = underTest
+ .wifiNetwork
+ .onEach { latest = it }
+ .launchIn(this)
+
+ val capabilities = mock<NetworkCapabilities>().apply {
+ whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+ }
+
+ getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+ assertThat(latest is WifiNetworkModel.Active).isTrue()
+ val latestActive = latest as WifiNetworkModel.Active
+ assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
+ assertThat(latestActive.ssid).isEqualTo(SSID)
+
+ job.cancel()
+ }
+
+ @Test
fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = runBlocking(IMMEDIATE) {
var latest: WifiNetworkModel? = null
val job = underTest
@@ -809,12 +986,12 @@
}
private fun createWifiNetworkCapabilities(
- wifiInfo: WifiInfo,
+ transportInfo: TransportInfo,
isValidated: Boolean = true,
): NetworkCapabilities {
return mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(wifiInfo)
+ whenever(it.transportInfo).thenReturn(transportInfo)
whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(isValidated)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 01d59f9..089a170 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -84,7 +84,9 @@
@Test
fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
+ wifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1)
+ )
var latest: String? = "default"
val job = underTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 726e813..b932837 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -206,7 +206,8 @@
// Enabled = false => no networks shown
TestCase(
enabled = false,
- network = WifiNetworkModel.CarrierMerged,
+ network =
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
expected = null,
),
TestCase(
@@ -228,7 +229,8 @@
// forceHidden = true => no networks shown
TestCase(
forceHidden = true,
- network = WifiNetworkModel.CarrierMerged,
+ network =
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
expected = null,
),
TestCase(
@@ -369,7 +371,8 @@
// network = CarrierMerged => not shown
TestCase(
- network = WifiNetworkModel.CarrierMerged,
+ network =
+ WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1),
expected = null,
),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 4b32ee2..0cca7b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -390,19 +390,27 @@
bindController(view, row.getEntry());
view.setVisibility(View.GONE);
- View crossFadeView = new View(mContext);
+ View fadeOutView = new View(mContext);
+ fadeOutView.setId(com.android.internal.R.id.actions_container_layout);
+
+ FrameLayout parent = new FrameLayout(mContext);
+ parent.addView(view);
+ parent.addView(fadeOutView);
// Start focus animation
- view.focusAnimated(crossFadeView);
-
+ view.focusAnimated();
assertTrue(view.isAnimatingAppearance());
- // fast forward to end of animation
- mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+ // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f
+ mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1);
+ assertEquals(0f, fadeOutView.getAlpha());
- // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind
+ // fast forward to end of animation
+ mAnimatorTestRule.advanceTimeBy(1);
+
+ // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind
// RemoteInputView)
- assertEquals(1f, crossFadeView.getAlpha());
+ assertEquals(1f, fadeOutView.getAlpha());
assertFalse(view.isAnimatingAppearance());
assertEquals(View.VISIBLE, view.getVisibility());
assertEquals(1f, view.getAlpha());
@@ -415,20 +423,27 @@
mDependency,
TestableLooper.get(this));
ExpandableNotificationRow row = helper.createRow();
- FrameLayout remoteInputViewParent = new FrameLayout(mContext);
RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
- remoteInputViewParent.addView(view);
bindController(view, row.getEntry());
+ View fadeInView = new View(mContext);
+ fadeInView.setId(com.android.internal.R.id.actions_container_layout);
+
+ FrameLayout parent = new FrameLayout(mContext);
+ parent.addView(view);
+ parent.addView(fadeInView);
+
// Start defocus animation
- view.onDefocus(true, false);
+ view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */);
assertEquals(View.VISIBLE, view.getVisibility());
+ assertEquals(0f, fadeInView.getAlpha());
// fast forward to end of animation
mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
// assert that RemoteInputView is no longer visible
assertEquals(View.GONE, view.getVisibility());
+ assertEquals(1f, fadeInView.getAlpha());
}
// NOTE: because we're refactoring the RemoteInputView and moving logic into the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
copy to packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
index 67733e9..7e01088 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt
@@ -11,15 +11,15 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.keyguard.shared.model
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
+package com.android.systemui.stylus
-/** Animation parameters */
-data class AnimationParams(
- val startTime: Duration = 0.milliseconds,
- val duration: Duration,
-)
+import android.hardware.BatteryState
+
+class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
+ override fun getCapacity() = capacity
+ override fun getStatus() = 0
+ override fun isPresent() = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt
deleted file mode 100644
index 8dd088f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.stylus
-
-import android.content.Context
-import android.hardware.BatteryState
-import android.hardware.input.InputManager
-import android.os.Handler
-import android.testing.AndroidTestingRunner
-import android.view.InputDevice
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-@Ignore("TODO(b/20579491): unignore on main")
-class StylusFirstUsageListenerTest : SysuiTestCase() {
- @Mock lateinit var context: Context
- @Mock lateinit var inputManager: InputManager
- @Mock lateinit var stylusManager: StylusManager
- @Mock lateinit var featureFlags: FeatureFlags
- @Mock lateinit var internalStylusDevice: InputDevice
- @Mock lateinit var otherDevice: InputDevice
- @Mock lateinit var externalStylusDevice: InputDevice
- @Mock lateinit var batteryState: BatteryState
- @Mock lateinit var handler: Handler
-
- private lateinit var stylusListener: StylusFirstUsageListener
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
- whenever(inputManager.isStylusEverUsed(context)).thenReturn(false)
-
- stylusListener =
- StylusFirstUsageListener(
- context,
- inputManager,
- stylusManager,
- featureFlags,
- EXECUTOR,
- handler
- )
- stylusListener.hasStarted = false
-
- whenever(handler.post(any())).thenAnswer {
- (it.arguments[0] as Runnable).run()
- true
- }
-
- whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
- whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
- whenever(internalStylusDevice.isExternal).thenReturn(false)
- whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
- whenever(externalStylusDevice.isExternal).thenReturn(true)
-
- whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
- whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
- whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID))
- .thenReturn(internalStylusDevice)
- whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID))
- .thenReturn(externalStylusDevice)
- }
-
- @Test
- fun start_flagDisabled_doesNotRegister() {
- whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false)
-
- stylusListener.start()
-
- verify(stylusManager, never()).registerCallback(any())
- verify(inputManager, never()).setStylusEverUsed(context, true)
- }
-
- @Test
- fun start_toggleHasStarted() {
- stylusListener.start()
-
- assert(stylusListener.hasStarted)
- }
-
- @Test
- fun start_hasStarted_doesNotRegister() {
- stylusListener.hasStarted = true
-
- stylusListener.start()
-
- verify(stylusManager, never()).registerCallback(any())
- }
-
- @Test
- fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
- whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID))
-
- stylusListener.start()
-
- verify(stylusManager, never()).registerCallback(any())
- verify(inputManager, never()).setStylusEverUsed(context, true)
- }
-
- @Test
- fun start_stylusEverUsed_doesNotRegister() {
- whenever(inputManager.inputDeviceIds)
- .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
- whenever(inputManager.isStylusEverUsed(context)).thenReturn(true)
-
- stylusListener.start()
-
- verify(stylusManager, never()).registerCallback(any())
- verify(inputManager, never()).setStylusEverUsed(context, true)
- }
-
- @Test
- fun start_hostDeviceSupportsStylus_registersListener() {
- whenever(inputManager.inputDeviceIds)
- .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
-
- stylusListener.start()
-
- verify(stylusManager).registerCallback(any())
- verify(inputManager, never()).setStylusEverUsed(context, true)
- }
-
- @Test
- fun onStylusAdded_hasNotStarted_doesNotRegisterListener() {
- stylusListener.hasStarted = false
-
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
- verifyZeroInteractions(inputManager)
- }
-
- @Test
- fun onStylusAdded_internalStylus_registersListener() {
- stylusListener.hasStarted = true
-
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
- verify(inputManager, times(1))
- .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener)
- }
-
- @Test
- fun onStylusAdded_externalStylus_doesNotRegisterListener() {
- stylusListener.hasStarted = true
-
- stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID)
-
- verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
- }
-
- @Test
- fun onStylusAdded_otherDevice_doesNotRegisterListener() {
- stylusListener.onStylusAdded(OTHER_DEVICE_ID)
-
- verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
- }
-
- @Test
- fun onStylusRemoved_registeredDevice_unregistersListener() {
- stylusListener.hasStarted = true
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
- stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
-
- verify(inputManager, times(1))
- .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
- }
-
- @Test
- fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() {
- stylusListener.hasStarted = false
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
- stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
-
- verifyZeroInteractions(inputManager)
- }
-
- @Test
- fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() {
- stylusListener.hasStarted = true
-
- stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
-
- verifyNoMoreInteractions(inputManager)
- }
-
- @Test
- fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() {
- stylusListener.hasStarted = true
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
- stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
-
- verify(inputManager).setStylusEverUsed(context, true)
- verify(inputManager, times(1))
- .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
- verify(stylusManager).unregisterCallback(stylusListener)
- }
-
- @Test
- fun onStylusBluetoothConnected_hasNotStarted_doesNoting() {
- stylusListener.hasStarted = false
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
-
- stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
-
- verifyZeroInteractions(inputManager)
- verifyZeroInteractions(stylusManager)
- }
-
- @Test
- fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() {
- stylusListener.hasStarted = true
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
- whenever(batteryState.isPresent).thenReturn(true)
-
- stylusListener.onBatteryStateChanged(0, 1, batteryState)
-
- verify(inputManager).setStylusEverUsed(context, true)
- verify(inputManager, times(1))
- .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
- verify(stylusManager).unregisterCallback(stylusListener)
- }
-
- @Test
- fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() {
- stylusListener.hasStarted = true
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
- whenever(batteryState.isPresent).thenReturn(false)
-
- stylusListener.onBatteryStateChanged(0, 1, batteryState)
-
- verifyZeroInteractions(stylusManager)
- verify(inputManager, never())
- .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
- }
-
- @Test
- fun onBatteryStateChanged_hasNotStarted_doesNothing() {
- stylusListener.hasStarted = false
- stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
- whenever(batteryState.isPresent).thenReturn(false)
-
- stylusListener.onBatteryStateChanged(0, 1, batteryState)
-
- verifyZeroInteractions(inputManager)
- verifyZeroInteractions(stylusManager)
- }
-
- companion object {
- private const val OTHER_DEVICE_ID = 0
- private const val INTERNAL_STYLUS_DEVICE_ID = 1
- private const val EXTERNAL_STYLUS_DEVICE_ID = 2
- private val EXECUTOR = FakeExecutor(FakeSystemClock())
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
index 984de5b..6d6e40a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt
@@ -17,12 +17,15 @@
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
+import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.view.InputDevice
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.Executor
@@ -31,30 +34,27 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
-@Ignore("b/257936830 until bt APIs")
class StylusManagerTest : SysuiTestCase() {
@Mock lateinit var inputManager: InputManager
-
@Mock lateinit var stylusDevice: InputDevice
-
@Mock lateinit var btStylusDevice: InputDevice
-
@Mock lateinit var otherDevice: InputDevice
-
+ @Mock lateinit var batteryState: BatteryState
@Mock lateinit var bluetoothAdapter: BluetoothAdapter
-
@Mock lateinit var bluetoothDevice: BluetoothDevice
-
@Mock lateinit var handler: Handler
+ @Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var stylusCallback: StylusManager.StylusCallback
@@ -75,11 +75,8 @@
true
}
- stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR)
-
- stylusManager.registerCallback(stylusCallback)
-
- stylusManager.registerBatteryCallback(stylusBatteryCallback)
+ stylusManager =
+ StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
@@ -92,19 +89,47 @@
whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice)
whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice)
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID))
+ whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(false)
whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice)
whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS)
+
+ whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
+
+ stylusManager.startListener()
+ stylusManager.registerCallback(stylusCallback)
+ stylusManager.registerBatteryCallback(stylusBatteryCallback)
+ clearInvocations(inputManager)
}
@Test
- fun startListener_registersInputDeviceListener() {
+ fun startListener_hasNotStarted_registersInputDeviceListener() {
+ stylusManager =
+ StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+
stylusManager.startListener()
verify(inputManager, times(1)).registerInputDeviceListener(any(), any())
}
@Test
+ fun startListener_hasStarted_doesNothing() {
+ stylusManager.startListener()
+
+ verifyZeroInteractions(inputManager)
+ }
+
+ @Test
+ fun onInputDeviceAdded_hasNotStarted_doesNothing() {
+ stylusManager =
+ StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+
+ stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+ verifyZeroInteractions(stylusCallback)
+ }
+
+ @Test
fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() {
stylusManager.registerCallback(otherStylusCallback)
@@ -117,6 +142,26 @@
}
@Test
+ fun onInputDeviceAdded_internalStylus_registersBatteryListener() {
+ whenever(stylusDevice.isExternal).thenReturn(false)
+
+ stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+ verify(inputManager, times(1))
+ .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager)
+ }
+
+ @Test
+ fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() {
+ whenever(stylusDevice.isExternal).thenReturn(true)
+
+ stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+ verify(inputManager, never())
+ .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager)
+ }
+
+ @Test
fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
@@ -125,6 +170,23 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
+ fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() {
+ stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
+
+ verify(stylusCallback, times(1)).onStylusFirstUsed()
+ }
+
+ @Test
+ @Ignore("b/257936830 until bt APIs")
+ fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() {
+ stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
+
+ verify(inputManager, times(1)).setStylusEverUsed(mContext, true)
+ }
+
+ @Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -143,6 +205,17 @@
}
@Test
+ fun onInputDeviceChanged_hasNotStarted_doesNothing() {
+ stylusManager =
+ StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+
+ stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
+
+ verifyZeroInteractions(stylusCallback)
+ }
+
+ @Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
// whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
@@ -157,6 +230,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
// whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
@@ -168,6 +242,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
// whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
@@ -179,6 +254,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -189,6 +265,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
@@ -198,6 +275,17 @@
}
@Test
+ fun onInputDeviceRemoved_hasNotStarted_doesNothing() {
+ stylusManager =
+ StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
+ stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+ stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
+
+ verifyZeroInteractions(stylusCallback)
+ }
+
+ @Test
fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
stylusManager.registerCallback(otherStylusCallback)
@@ -219,6 +307,17 @@
}
@Test
+ fun onInputDeviceRemoved_unregistersBatteryListener() {
+ stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
+
+ stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
+
+ verify(inputManager, times(1))
+ .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
+ }
+
+ @Test
+ @Ignore("b/257936830 until bt APIs")
fun onInputDeviceRemoved_btStylus_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -232,6 +331,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothConnected_registersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -239,6 +339,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)
@@ -248,6 +349,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -257,6 +359,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.registerBatteryCallback(otherStylusBatteryCallback)
@@ -274,6 +377,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -288,6 +392,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -302,6 +407,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
stylusManager.onMetadataChanged(
bluetoothDevice,
@@ -313,6 +419,7 @@
}
@Test
+ @Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -326,6 +433,63 @@
.onStylusBluetoothChargingStateChanged(any(), any(), any())
}
+ @Test
+ @Ignore("TODO(b/261826950): remove on main")
+ fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() {
+ whenever(batteryState.isPresent).thenReturn(true)
+
+ stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+ verify(inputManager).setStylusEverUsed(mContext, true)
+ }
+
+ @Test
+ @Ignore("TODO(b/261826950): remove on main")
+ fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() {
+ whenever(batteryState.isPresent).thenReturn(true)
+
+ stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+ verify(stylusCallback, times(1)).onStylusFirstUsed()
+ }
+
+ @Test
+ @Ignore("TODO(b/261826950): remove on main")
+ fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() {
+ whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true)
+ whenever(batteryState.isPresent).thenReturn(true)
+
+ stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+ verify(inputManager, never()).setStylusEverUsed(mContext, true)
+ }
+
+ @Test
+ @Ignore("TODO(b/261826950): remove on main")
+ fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() {
+ whenever(batteryState.isPresent).thenReturn(false)
+
+ stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+ verify(inputManager, never())
+ .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
+ }
+
+ @Test
+ fun onBatteryStateChanged_hasNotStarted_doesNothing() {
+ stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+ verifyZeroInteractions(inputManager)
+ }
+
+ @Test
+ fun onBatteryStateChanged_executesBatteryCallbacks() {
+ stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+
+ verify(stylusBatteryCallback, times(1))
+ .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
+ }
+
companion object {
private val EXECUTOR = Executor { r -> r.run() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
index ff382a3..cc6be5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt
@@ -25,17 +25,15 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.whenever
-import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@@ -60,7 +58,6 @@
inputManager,
stylusUsiPowerUi,
featureFlags,
- DIRECT_EXECUTOR,
)
whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true)
@@ -79,40 +76,33 @@
}
@Test
- fun start_addsBatteryListenerForInternalStylus() {
+ fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
+ whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID))
+
startable.start()
- verify(inputManager, times(1))
- .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
+ verifyZeroInteractions(stylusManager)
}
@Test
- fun onStylusAdded_internalStylus_addsBatteryListener() {
+ fun start_initStylusUsiPowerUi() {
+ startable.start()
+
+ verify(stylusUsiPowerUi, times(1)).init()
+ }
+
+ @Test
+ fun onStylusAdded_internal_updatesNotificationSuppression() {
startable.onStylusAdded(STYLUS_DEVICE_ID)
- verify(inputManager, times(1))
- .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
+ verify(stylusUsiPowerUi, times(1)).updateSuppression(false)
}
@Test
- fun onStylusAdded_externalStylus_doesNotAddBatteryListener() {
+ fun onStylusAdded_external_noop() {
startable.onStylusAdded(EXTERNAL_DEVICE_ID)
- verify(inputManager, never())
- .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable)
- }
-
- @Test
- fun onStylusRemoved_registeredStylus_removesBatteryListener() {
- startable.onStylusAdded(STYLUS_DEVICE_ID)
- startable.onStylusRemoved(STYLUS_DEVICE_ID)
-
- inOrder(inputManager).let {
- it.verify(inputManager, times(1))
- .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
- it.verify(inputManager, times(1))
- .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable)
- }
+ verifyZeroInteractions(stylusUsiPowerUi)
}
@Test
@@ -130,28 +120,34 @@
}
@Test
- fun onBatteryStateChanged_batteryPresent_refreshesNotification() {
- val batteryState = mock(BatteryState::class.java)
- whenever(batteryState.isPresent).thenReturn(true)
+ fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() {
+ val batteryState = FixedCapacityBatteryState(0.1f)
- startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+ startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
- verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState)
+ verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState)
}
@Test
- fun onBatteryStateChanged_batteryNotPresent_noop() {
+ fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() {
+ val batteryState = FixedCapacityBatteryState(0f)
+
+ startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+
+ verifyNoMoreInteractions(stylusUsiPowerUi)
+ }
+
+ @Test
+ fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() {
val batteryState = mock(BatteryState::class.java)
whenever(batteryState.isPresent).thenReturn(false)
- startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
+ startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
verifyNoMoreInteractions(stylusUsiPowerUi)
}
companion object {
- private val DIRECT_EXECUTOR = Executor { r -> r.run() }
-
private const val EXTERNAL_DEVICE_ID = 0
private const val STYLUS_DEVICE_ID = 1
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
index 5987550..1e81dc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
@@ -16,8 +16,12 @@
package com.android.systemui.stylus
-import android.hardware.BatteryState
+import android.app.Notification
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
import android.hardware.input.InputManager
+import android.os.Bundle
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.view.InputDevice
@@ -26,14 +30,22 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.doNothing
import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
@@ -46,13 +58,19 @@
@Mock lateinit var inputManager: InputManager
@Mock lateinit var handler: Handler
@Mock lateinit var btStylusDevice: InputDevice
+ @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification>
private lateinit var stylusUsiPowerUi: StylusUsiPowerUI
+ private lateinit var broadcastReceiver: BroadcastReceiver
+ private lateinit var contextSpy: Context
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ contextSpy = spy(mContext)
+ doNothing().whenever(contextSpy).startActivity(any())
+
whenever(handler.post(any())).thenAnswer {
(it.arguments[0] as Runnable).run()
true
@@ -63,56 +81,77 @@
whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
// whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES")
- stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler)
+ stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler)
+ broadcastReceiver = stylusUsiPowerUi.receiver
+ }
+
+ @Test
+ fun updateBatteryState_capacityZero_noop() {
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f))
+
+ verifyNoMoreInteractions(notificationManager)
}
@Test
fun updateBatteryState_capacityBelowThreshold_notifies() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
- verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
+ verify(notificationManager, times(1))
+ .notify(eq(R.string.stylus_battery_low_percentage), any())
verifyNoMoreInteractions(notificationManager)
}
@Test
fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
- verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+ verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
verifyNoMoreInteractions(notificationManager)
}
@Test
fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
inOrder(notificationManager).let {
- it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
- it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+ it.verify(notificationManager, times(1))
+ .notify(eq(R.string.stylus_battery_low_percentage), any())
+ it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
it.verifyNoMoreInteractions()
}
}
@Test
fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f))
- verify(notificationManager, times(2)).notify(eq(R.string.stylus_battery_low), any())
+ verify(notificationManager, times(2))
+ .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture())
+ assertEquals(
+ notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE),
+ context.getString(R.string.stylus_battery_low_percentage, "15%")
+ )
+ assertEquals(
+ notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT),
+ context.getString(R.string.stylus_battery_low_subtitle)
+ )
verifyNoMoreInteractions(notificationManager)
}
@Test
fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f))
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
inOrder(notificationManager).let {
- it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
- it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
- it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
+ it.verify(notificationManager, times(1))
+ .notify(eq(R.string.stylus_battery_low_percentage), any())
+ it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
+ it.verify(notificationManager, times(1))
+ .notify(eq(R.string.stylus_battery_low_percentage), any())
it.verifyNoMoreInteractions()
}
}
@@ -121,47 +160,66 @@
fun updateSuppression_noExistingNotification_cancelsNotification() {
stylusUsiPowerUi.updateSuppression(true)
- verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+ verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
verifyNoMoreInteractions(notificationManager)
}
@Test
fun updateSuppression_existingNotification_cancelsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
stylusUsiPowerUi.updateSuppression(true)
inOrder(notificationManager).let {
- it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any())
- it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low)
+ it.verify(notificationManager, times(1))
+ .notify(eq(R.string.stylus_battery_low_percentage), any())
+ it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage)
it.verifyNoMoreInteractions()
}
}
@Test
@Ignore("TODO(b/257936830): get bt address once input api available")
- fun refresh_hasConnectedBluetoothStylus_doesNotNotify() {
+ fun refresh_hasConnectedBluetoothStylus_cancelsNotification() {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
stylusUsiPowerUi.refresh()
- verifyNoMoreInteractions(notificationManager)
+ verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
}
@Test
@Ignore("TODO(b/257936830): get bt address once input api available")
fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() {
- stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f))
+ stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0))
stylusUsiPowerUi.refresh()
- verify(notificationManager).cancel(R.string.stylus_battery_low)
+ verify(notificationManager).cancel(R.string.stylus_battery_low_percentage)
}
- class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() {
- override fun getCapacity() = capacity
- override fun getStatus() = 0
- override fun isPresent() = true
+ @Test
+ fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() {
+ val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+ val activityIntentCaptor = argumentCaptor<Intent>()
+ stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f))
+ broadcastReceiver.onReceive(contextSpy, intent)
+
+ verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture())
+ assertThat(activityIntentCaptor.value.action)
+ .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS)
+ val args =
+ activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS)
+ as Bundle
+ assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1)
+ }
+
+ @Test
+ fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() {
+ val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY)
+ broadcastReceiver.onReceive(contextSpy, intent)
+
+ verify(contextSpy, never()).startActivity(any())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 99e2012..c7c6b94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -159,7 +159,7 @@
underTest.displayView(getState())
assertThat(fakeWakeLock.isHeld).isTrue()
- underTest.removeView("id", "test reason")
+ underTest.removeView(DEFAULT_ID, "test reason")
assertThat(fakeWakeLock.isHeld).isFalse()
}
@@ -175,6 +175,8 @@
@Test
fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "name",
@@ -199,10 +201,15 @@
assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0])
+ // Since the controller is still storing the older view in case it'll get re-displayed
+ // later, the listener shouldn't be notified
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun displayView_viewDoesNotDisappearsBeforeTimeout() {
+ val listener = registerListener()
+
val state = getState()
underTest.displayView(state)
reset(windowManager)
@@ -210,10 +217,13 @@
fakeClock.advanceTime(TIMEOUT_MS - 1)
verify(windowManager, never()).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun displayView_viewDisappearsAfterTimeout() {
+ val listener = registerListener()
+
val state = getState()
underTest.displayView(state)
reset(windowManager)
@@ -221,10 +231,13 @@
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
fun displayView_calledAgainBeforeTimeout_timeoutReset() {
+ val listener = registerListener()
+
// First, display the view
val state = getState()
underTest.displayView(state)
@@ -239,10 +252,13 @@
// Verify we didn't hide the view
verify(windowManager, never()).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun displayView_calledAgainBeforeTimeout_eventuallyTimesOut() {
+ val listener = registerListener()
+
// First, display the view
val state = getState()
underTest.displayView(state)
@@ -255,6 +271,7 @@
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
@@ -271,25 +288,9 @@
}
@Test
- fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() {
- var runnable1Run = false
- underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
- runnable1Run = true
- }
-
- var runnable2Run = false
- underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) {
- runnable2Run = true
- }
-
- fakeClock.advanceTime(TIMEOUT_MS + 1)
-
- assertThat(runnable1Run).isFalse()
- assertThat(runnable2Run).isTrue()
- }
-
- @Test
fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "name",
@@ -315,10 +316,16 @@
assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
verify(windowManager).removeView(viewCaptor.allValues[0])
verify(configurationController, never()).removeCallback(any())
+
+ // Since the controller is still storing the older view in case it'll get re-displayed
+ // later, the listener shouldn't be notified
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
- fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
+ fun multipleViewsWithDifferentIds_newViewRemoved_previousViewIsDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any())
@@ -329,24 +336,35 @@
verify(windowManager).removeView(any())
verify(windowManager).addView(any(), any())
reset(windowManager)
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
+ // WHEN the current view is removed
underTest.removeView("id2", "test reason")
+ // THEN it's correctly removed
verify(windowManager).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly("id2")
+
+ // And the previous view is correctly added
verify(windowManager).addView(any(), any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
+ // WHEN the previous view times out
reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1)
+ // THEN it is also removed
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id2", "id1"))
}
@Test
fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any())
@@ -361,7 +379,8 @@
// WHEN an old view is removed
underTest.removeView("id1", "test reason")
- // THEN we don't update anything
+ // THEN we don't update anything except the listener
+ assertThat(listener.permanentlyRemovedIds).containsExactly("id1")
verify(windowManager, never()).removeView(any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
@@ -372,10 +391,13 @@
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id1", "id2"))
}
@Test
fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(ViewInfo("First name", id = "id1"))
underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.displayView(ViewInfo("Third name", id = "id3"))
@@ -387,6 +409,7 @@
underTest.removeView("id3", "test reason")
verify(windowManager).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3"))
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
verify(configurationController, never()).removeCallback(any())
@@ -395,6 +418,7 @@
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2"))
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
verify(configurationController, never()).removeCallback(any())
@@ -403,6 +427,7 @@
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).isEqualTo(listOf("id3", "id2", "id1"))
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
}
@@ -438,6 +463,8 @@
@Test
fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000))
@@ -451,10 +478,13 @@
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2", "id3")
}
@Test
fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000))
fakeClock.advanceTime(1000)
underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500))
@@ -467,10 +497,13 @@
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0)
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
}
@Test
fun lowerThenHigherPriority_higherReplacesLower() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "normal",
@@ -499,10 +532,15 @@
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title")
verify(configurationController, never()).removeCallback(any())
+ // Since the controller is still storing the older view in case it'll get re-displayed
+ // later, the listener shouldn't be notified
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun lowerThenHigherPriority_lowerPriorityRedisplayed() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "normal",
@@ -537,6 +575,7 @@
// THEN the normal view is re-displayed
verify(windowManager).removeView(viewCaptor.allValues[1])
+ assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(any(), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
verify(configurationController, never()).removeCallback(any())
@@ -544,6 +583,8 @@
@Test
fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "normal",
@@ -573,6 +614,7 @@
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
}
@Test
@@ -609,6 +651,8 @@
@Test
fun higherThenLowerPriority_lowerEventuallyDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "critical",
@@ -644,6 +688,7 @@
// THEN the second normal view is displayed
verify(windowManager).removeView(viewCaptor.value)
+ assertThat(listener.permanentlyRemovedIds).containsExactly("critical")
verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor))
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title")
assertThat(underTest.activeViews.size).isEqualTo(1)
@@ -652,6 +697,8 @@
@Test
fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "critical",
@@ -691,10 +738,13 @@
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews).isEmpty()
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly("critical", "normal")
}
@Test
fun criticalThenNewCritical_newCriticalDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "critical 1",
@@ -724,10 +774,15 @@
assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any())
+ // Since the controller is still storing the older view in case it'll get re-displayed
+ // later, the listener shouldn't be notified
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
fun normalThenNewNormal_newNormalDisplayed() {
+ val listener = registerListener()
+
underTest.displayView(
ViewInfo(
name = "normal 1",
@@ -757,6 +812,9 @@
assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2")
assertThat(underTest.activeViews.size).isEqualTo(2)
verify(configurationController, never()).removeCallback(any())
+ // Since the controller is still storing the older view in case it'll get re-displayed
+ // later, the listener shouldn't be notified
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
}
@Test
@@ -957,25 +1015,103 @@
}
@Test
- fun removeView_viewRemovedAndRemovalLogged() {
+ fun removeView_viewRemovedAndRemovalLoggedAndListenerNotified() {
+ val listener = registerListener()
+
// First, add the view
underTest.displayView(getState())
// Then, remove it
val reason = "test reason"
- val deviceId = "id"
- underTest.removeView(deviceId, reason)
+ underTest.removeView(DEFAULT_ID, reason)
verify(windowManager).removeView(any())
- verify(logger).logViewRemoval(deviceId, reason)
+ verify(logger).logViewRemoval(DEFAULT_ID, reason)
verify(configurationController).removeCallback(any())
+ assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
}
@Test
- fun removeView_noAdd_viewNotRemoved() {
+ fun removeView_noAdd_viewNotRemovedAndListenerNotNotified() {
+ val listener = registerListener()
+
underTest.removeView("id", "reason")
verify(windowManager, never()).removeView(any())
+ assertThat(listener.permanentlyRemovedIds).isEmpty()
+ }
+
+ @Test
+ fun listenerRegistered_notifiedOnRemoval() {
+ val listener = registerListener()
+ underTest.displayView(getState())
+
+ underTest.removeView(DEFAULT_ID, "reason")
+
+ assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+ }
+
+ @Test
+ fun listenerRegistered_notifiedOnTimedOutEvenWhenNotDisplayed() {
+ val listener = registerListener()
+ underTest.displayView(
+ ViewInfo(
+ id = "id1",
+ name = "name1",
+ timeoutMs = 3000,
+ ),
+ )
+
+ // Display a second view
+ underTest.displayView(
+ ViewInfo(
+ id = "id2",
+ name = "name2",
+ timeoutMs = 2500,
+ ),
+ )
+
+ // WHEN the second view times out
+ fakeClock.advanceTime(2501)
+
+ // THEN the listener is notified of both IDs, since id2 timed out and id1 doesn't have
+ // enough time left to be redisplayed
+ assertThat(listener.permanentlyRemovedIds).containsExactly("id1", "id2")
+ }
+
+ @Test
+ fun multipleListeners_allNotified() {
+ val listener1 = registerListener()
+ val listener2 = registerListener()
+ val listener3 = registerListener()
+
+ underTest.displayView(getState())
+
+ underTest.removeView(DEFAULT_ID, "reason")
+
+ assertThat(listener1.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+ assertThat(listener2.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+ assertThat(listener3.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+ }
+
+ @Test
+ fun sameListenerRegisteredMultipleTimes_onlyNotifiedOnce() {
+ val listener = registerListener()
+ underTest.registerListener(listener)
+ underTest.registerListener(listener)
+
+ underTest.displayView(getState())
+
+ underTest.removeView(DEFAULT_ID, "reason")
+
+ assertThat(listener.permanentlyRemovedIds).hasSize(1)
+ assertThat(listener.permanentlyRemovedIds).containsExactly(DEFAULT_ID)
+ }
+
+ private fun registerListener(): Listener {
+ return Listener().also {
+ underTest.registerListener(it)
+ }
}
private fun getState(name: String = "name") = ViewInfo(name)
@@ -1030,9 +1166,17 @@
override val windowTitle: String = "Window Title",
override val wakeReason: String = "WAKE_REASON",
override val timeoutMs: Int = TIMEOUT_MS.toInt(),
- override val id: String = "id",
+ override val id: String = DEFAULT_ID,
override val priority: ViewPriority = ViewPriority.NORMAL,
) : TemporaryViewInfo()
+
+ inner class Listener : TemporaryViewDisplayController.Listener {
+ val permanentlyRemovedIds = mutableListOf<String>()
+ override fun onInfoPermanentlyRemoved(id: String, reason: String) {
+ permanentlyRemovedIds.add(id)
+ }
+ }
}
private const val TIMEOUT_MS = 10000L
+private const val DEFAULT_ID = "defaultId"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 90178c6..fc7436a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -20,6 +20,7 @@
import android.os.VibrationEffect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
@@ -43,6 +44,8 @@
import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.view.ViewUtil
@@ -54,6 +57,7 @@
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -62,7 +66,7 @@
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class ChipbarCoordinatorTest : SysuiTestCase() {
- private lateinit var underTest: FakeChipbarCoordinator
+ private lateinit var underTest: ChipbarCoordinator
@Mock private lateinit var logger: ChipbarLogger
@Mock private lateinit var accessibilityManager: AccessibilityManager
@@ -74,6 +78,8 @@
@Mock private lateinit var falsingCollector: FalsingCollector
@Mock private lateinit var viewUtil: ViewUtil
@Mock private lateinit var vibratorHelper: VibratorHelper
+ @Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
+ private lateinit var chipbarAnimator: TestChipbarAnimator
private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
private lateinit var fakeWakeLock: WakeLockFake
private lateinit var fakeClock: FakeSystemClock
@@ -93,9 +99,10 @@
fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
uiEventLoggerFake = UiEventLoggerFake()
+ chipbarAnimator = TestChipbarAnimator()
underTest =
- FakeChipbarCoordinator(
+ ChipbarCoordinator(
context,
logger,
windowManager,
@@ -104,8 +111,10 @@
configurationController,
dumpManager,
powerManager,
+ chipbarAnimator,
falsingManager,
falsingCollector,
+ swipeGestureHandler,
viewUtil,
vibratorHelper,
fakeWakeLockBuilder,
@@ -365,6 +374,26 @@
verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
}
+ /** Regression test for b/266119467. */
+ @Test
+ fun displayView_animationFailure_viewsStillBecomeVisible() {
+ chipbarAnimator.allowAnimation = false
+
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.id.check_box, null),
+ Text.Loaded("text"),
+ endItem = ChipbarEndItem.Loading,
+ )
+ )
+
+ val view = getChipbarView()
+ assertThat(view.getInnerView().alpha).isEqualTo(1f)
+ assertThat(view.getStartIconView().alpha).isEqualTo(1f)
+ assertThat(view.getLoadingIcon().alpha).isEqualTo(1f)
+ assertThat(view.getChipTextView().alpha).isEqualTo(1f)
+ }
+
@Test
fun updateView_viewUpdated() {
// First, display a view
@@ -430,17 +459,137 @@
verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any())
}
+ /** Regression test for b/266209420. */
+ @Test
+ fun displayViewThenImmediateRemoval_viewStillRemoved() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Error,
+ ),
+ )
+ val chipbarView = getChipbarView()
+
+ underTest.removeView(DEVICE_ID, "test reason")
+
+ verify(windowManager).removeView(chipbarView)
+ }
+
+ /** Regression test for b/266209420. */
+ @Test
+ fun removeView_animationFailure_viewStillRemoved() {
+ chipbarAnimator.allowAnimation = false
+
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Error,
+ ),
+ )
+ val chipbarView = getChipbarView()
+
+ underTest.removeView(DEVICE_ID, "test reason")
+
+ verify(windowManager).removeView(chipbarView)
+ }
+
+ @Test
+ fun swipeToDismiss_false_neverListensForGesture() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Loading,
+ allowSwipeToDismiss = false,
+ )
+ )
+
+ verify(swipeGestureHandler, never()).addOnGestureDetectedCallback(any(), any())
+ }
+
+ @Test
+ fun swipeToDismiss_true_listensForGesture() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Loading,
+ allowSwipeToDismiss = true,
+ )
+ )
+
+ verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), any())
+ }
+
+ @Test
+ fun swipeToDismiss_swipeOccurs_viewDismissed() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Loading,
+ allowSwipeToDismiss = true,
+ )
+ )
+ val view = getChipbarView()
+
+ val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
+ verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
+
+ callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
+
+ verify(windowManager).removeView(view)
+ }
+
+ @Test
+ fun swipeToDismiss_viewUpdatedToFalse_swipeOccurs_viewNotDismissed() {
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Loading,
+ allowSwipeToDismiss = true,
+ )
+ )
+ val view = getChipbarView()
+ val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
+ verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
+
+ // WHEN the view is updated to not allow swipe-to-dismiss
+ underTest.displayView(
+ createChipbarInfo(
+ Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+ Text.Loaded("title text"),
+ endItem = ChipbarEndItem.Loading,
+ allowSwipeToDismiss = false,
+ )
+ )
+
+ // THEN the callback is removed
+ verify(swipeGestureHandler).removeOnGestureDetectedCallback(any())
+
+ // And WHEN the old callback is invoked
+ callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
+
+ // THEN it is ignored and view isn't removed
+ verify(windowManager, never()).removeView(view)
+ }
+
private fun createChipbarInfo(
startIcon: Icon,
text: Text,
endItem: ChipbarEndItem?,
vibrationEffect: VibrationEffect? = null,
+ allowSwipeToDismiss: Boolean = false,
): ChipbarInfo {
return ChipbarInfo(
TintedIcon(startIcon, tintAttr = null),
text,
endItem,
vibrationEffect,
+ allowSwipeToDismiss,
windowTitle = WINDOW_TITLE,
wakeReason = WAKE_REASON,
timeoutMs = TIMEOUT,
@@ -453,8 +602,9 @@
private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
- private fun ViewGroup.getChipText(): String =
- (this.requireViewById<TextView>(R.id.text)).text as String
+ private fun ViewGroup.getChipTextView() = this.requireViewById<TextView>(R.id.text)
+
+ private fun ViewGroup.getChipText(): String = this.getChipTextView().text as String
private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
@@ -467,6 +617,25 @@
verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as ViewGroup
}
+
+ /** Test class that lets us disallow animations. */
+ inner class TestChipbarAnimator : ChipbarAnimator() {
+ var allowAnimation: Boolean = true
+
+ override fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ if (!allowAnimation) {
+ return false
+ }
+ return super.animateViewIn(innerView, onAnimationEnd)
+ }
+
+ override fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
+ if (!allowAnimation) {
+ return false
+ }
+ return super.animateViewOut(innerView, onAnimationEnd)
+ }
+ }
}
private const val TIMEOUT = 10000
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
deleted file mode 100644
index 4ef4e6c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.temporarydisplay.chipbar
-
-import android.content.Context
-import android.os.PowerManager
-import android.view.ViewGroup
-import android.view.WindowManager
-import android.view.accessibility.AccessibilityManager
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.util.view.ViewUtil
-import com.android.systemui.util.wakelock.WakeLock
-
-/** A fake implementation of [ChipbarCoordinator] for testing. */
-class FakeChipbarCoordinator(
- context: Context,
- logger: ChipbarLogger,
- windowManager: WindowManager,
- mainExecutor: DelayableExecutor,
- accessibilityManager: AccessibilityManager,
- configurationController: ConfigurationController,
- dumpManager: DumpManager,
- powerManager: PowerManager,
- falsingManager: FalsingManager,
- falsingCollector: FalsingCollector,
- viewUtil: ViewUtil,
- vibratorHelper: VibratorHelper,
- wakeLockBuilder: WakeLock.Builder,
- systemClock: SystemClock,
-) :
- ChipbarCoordinator(
- context,
- logger,
- windowManager,
- mainExecutor,
- accessibilityManager,
- configurationController,
- dumpManager,
- powerManager,
- falsingManager,
- falsingCollector,
- viewUtil,
- vibratorHelper,
- wakeLockBuilder,
- systemClock,
- ) {
- override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
- // Just bypass the animation in tests
- onAnimationEnd.run()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt
new file mode 100644
index 0000000..c539246
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandlerTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class SwipeChipbarAwayGestureHandlerTest : SysuiTestCase() {
+
+ private lateinit var underTest: SwipeChipbarAwayGestureHandler
+
+ @Before
+ fun setUp() {
+ underTest = SwipeChipbarAwayGestureHandler(context, FakeDisplayTracker(mContext), mock())
+ }
+
+ @Test
+ fun startOfGestureIsWithinBounds_noViewFetcher_returnsFalse() {
+ assertThat(underTest.startOfGestureIsWithinBounds(createMotionEvent())).isFalse()
+ }
+
+ @Test
+ fun startOfGestureIsWithinBounds_usesViewFetcher_aboveBottom_returnsTrue() {
+ val view = createMockView()
+
+ underTest.setViewFetcher { view }
+
+ val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f)
+ assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
+ }
+
+ @Test
+ fun startOfGestureIsWithinBounds_usesViewFetcher_slightlyBelowBottom_returnsTrue() {
+ val view = createMockView()
+
+ underTest.setViewFetcher { view }
+
+ val motionEvent = createMotionEvent(y = VIEW_BOTTOM + 20f)
+ assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
+ }
+
+ @Test
+ fun startOfGestureIsWithinBounds_usesViewFetcher_tooFarDown_returnsFalse() {
+ val view = createMockView()
+
+ underTest.setViewFetcher { view }
+
+ val motionEvent = createMotionEvent(y = VIEW_BOTTOM * 4f)
+ assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse()
+ }
+
+ @Test
+ fun startOfGestureIsWithinBounds_viewFetcherReset_returnsFalse() {
+ val view = createMockView()
+
+ underTest.setViewFetcher { view }
+
+ val motionEvent = createMotionEvent(y = VIEW_BOTTOM - 100f)
+ assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isTrue()
+
+ underTest.resetViewFetcher()
+ assertThat(underTest.startOfGestureIsWithinBounds(motionEvent)).isFalse()
+ }
+
+ private fun createMotionEvent(y: Float = 0f): MotionEvent {
+ return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, y, 0)
+ }
+
+ private fun createMockView(): View {
+ return mock<View>().also {
+ doAnswer { invocation ->
+ val out: Rect = invocation.getArgument(0)
+ out.set(0, 0, 0, VIEW_BOTTOM)
+ null
+ }
+ .whenever(it)
+ .getBoundsOnScreen(any())
+ }
+ }
+
+ private companion object {
+ const val VIEW_BOTTOM = 455
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 2a93fff..1710709 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -172,7 +172,7 @@
verify(mDumpManager).registerDumpable(any(), any());
verify(mDeviceProvisionedController).addCallback(mDeviceProvisionedListener.capture());
verify(mSecureSettings).registerContentObserverForUser(
- eq(Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES)),
+ eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES),
eq(false), mSettingsObserver.capture(), eq(UserHandle.USER_ALL)
);
}
@@ -790,15 +790,15 @@
reset(mResources);
when(mResources.getColor(eq(android.R.color.system_accent1_500), any()))
- .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().get(6));
+ .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().getS500());
when(mResources.getColor(eq(android.R.color.system_accent2_500), any()))
- .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().get(6));
+ .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().getS500());
when(mResources.getColor(eq(android.R.color.system_accent3_500), any()))
- .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().get(6));
+ .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().getS500());
when(mResources.getColor(eq(android.R.color.system_neutral1_500), any()))
- .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().get(6));
+ .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().getS500());
when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
- .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().get(6));
+ .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().getS500());
// Defers event because we already have initial colors.
verify(mThemeOverlayApplier, never())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index 89402de..f4226bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.unfold.util.FoldableDeviceStates
@@ -73,6 +74,8 @@
@Mock lateinit var viewTreeObserver: ViewTreeObserver
+ @Mock private lateinit var commandQueue: CommandQueue
+
@Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener>
private lateinit var deviceStates: FoldableDeviceStates
@@ -102,7 +105,8 @@
}
keyguardRepository = FakeKeyguardRepository()
- val keyguardInteractor = KeyguardInteractor(repository = keyguardRepository)
+ val keyguardInteractor =
+ KeyguardInteractor(repository = keyguardRepository, commandQueue = commandQueue)
// Needs to be run on the main thread
runBlocking(IMMEDIATE) {
@@ -171,8 +175,10 @@
fold()
underTest.onScreenTurningOn({})
- underTest.onStartedWakingUp()
+ // The body of onScreenTurningOn is executed on fakeExecutor,
+ // run all pending tasks before calling the next method
fakeExecutor.runAllReady()
+ underTest.onStartedWakingUp()
verify(latencyTracker).onActionStart(any())
verify(latencyTracker).onActionCancel(any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
index c316402..4a28cd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
@@ -26,6 +26,10 @@
listeners.forEach { it.onTransitionFinished() }
}
+ override fun onTransitionFinishing() {
+ listeners.forEach { it.onTransitionFinishing() }
+ }
+
override fun onTransitionProgress(progress: Float) {
listeners.forEach { it.onTransitionProgress(progress) }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
new file mode 100644
index 0000000..d3fdbd9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.unfold
+
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UnfoldHapticsPlayerTest : SysuiTestCase() {
+
+ private val progressProvider = TestUnfoldTransitionProvider()
+ private val vibrator: Vibrator = mock()
+ private val testFoldProvider = TestFoldProvider()
+
+ private lateinit var player: UnfoldHapticsPlayer
+
+ @Before
+ fun before() {
+ player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, Runnable::run, vibrator)
+ }
+
+ @Test
+ fun testUnfoldingTransitionFinishingEarly_playsHaptics() {
+ testFoldProvider.onFoldUpdate(isFolded = true)
+ testFoldProvider.onFoldUpdate(isFolded = false)
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.5f)
+ progressProvider.onTransitionFinishing()
+
+ verify(vibrator).vibrate(any<VibrationEffect>())
+ }
+
+ @Test
+ fun testUnfoldingTransitionFinishingLate_doesNotPlayHaptics() {
+ testFoldProvider.onFoldUpdate(isFolded = true)
+ testFoldProvider.onFoldUpdate(isFolded = false)
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.99f)
+ progressProvider.onTransitionFinishing()
+
+ verify(vibrator, never()).vibrate(any<VibrationEffect>())
+ }
+
+ @Test
+ fun testFoldingAfterUnfolding_doesNotPlayHaptics() {
+ // Unfold
+ testFoldProvider.onFoldUpdate(isFolded = true)
+ testFoldProvider.onFoldUpdate(isFolded = false)
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.5f)
+ progressProvider.onTransitionFinishing()
+ progressProvider.onTransitionFinished()
+ clearInvocations(vibrator)
+
+ // Fold
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.5f)
+ progressProvider.onTransitionFinished()
+ testFoldProvider.onFoldUpdate(isFolded = true)
+
+ verify(vibrator, never()).vibrate(any<VibrationEffect>())
+ }
+
+ @Test
+ fun testUnfoldingAfterFoldingAndUnfolding_playsHaptics() {
+ // Unfold
+ testFoldProvider.onFoldUpdate(isFolded = true)
+ testFoldProvider.onFoldUpdate(isFolded = false)
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.5f)
+ progressProvider.onTransitionFinishing()
+ progressProvider.onTransitionFinished()
+
+ // Fold
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.5f)
+ progressProvider.onTransitionFinished()
+ testFoldProvider.onFoldUpdate(isFolded = true)
+ clearInvocations(vibrator)
+
+ // Unfold again
+ testFoldProvider.onFoldUpdate(isFolded = true)
+ testFoldProvider.onFoldUpdate(isFolded = false)
+ progressProvider.onTransitionStarted()
+ progressProvider.onTransitionProgress(0.5f)
+ progressProvider.onTransitionFinishing()
+ progressProvider.onTransitionFinished()
+
+ verify(vibrator).vibrate(any<VibrationEffect>())
+ }
+
+ private class TestFoldProvider : FoldProvider {
+ private val listeners = arrayListOf<FoldProvider.FoldCallback>()
+
+ override fun registerCallback(callback: FoldProvider.FoldCallback, executor: Executor) {
+ listeners += callback
+ }
+
+ override fun unregisterCallback(callback: FoldProvider.FoldCallback) {
+ listeners -= callback
+ }
+
+ fun onFoldUpdate(isFolded: Boolean) {
+ listeners.forEach { it.onFoldUpdated(isFolded) }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 034c618..ccf378a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,12 +17,17 @@
package com.android.systemui.user.data.repository
+import android.app.IActivityManager
+import android.app.UserSwitchObserver
import android.content.pm.UserInfo
+import android.os.IRemoteCallback
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.util.settings.FakeSettings
@@ -39,7 +44,14 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -48,6 +60,8 @@
class UserRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var manager: UserManager
+ @Mock private lateinit var activityManager: IActivityManager
+ @Captor private lateinit var userSwitchObserver: ArgumentCaptor<UserSwitchObserver>
private lateinit var underTest: UserRepositoryImpl
@@ -214,6 +228,34 @@
assertThat(selectedUserInfo?.id).isEqualTo(1)
}
+ @Test
+ fun userSwitchingInProgress_registersOnlyOneUserSwitchObserver() = runSelfCancelingTest {
+ underTest = create(this)
+
+ underTest.userSwitchingInProgress.launchIn(this)
+ underTest.userSwitchingInProgress.launchIn(this)
+ underTest.userSwitchingInProgress.launchIn(this)
+
+ verify(activityManager, times(1)).registerUserSwitchObserver(any(), anyString())
+ }
+
+ @Test
+ fun userSwitchingInProgress_propagatesStateFromActivityManager() = runSelfCancelingTest {
+ underTest = create(this)
+ verify(activityManager)
+ .registerUserSwitchObserver(userSwitchObserver.capture(), anyString())
+
+ userSwitchObserver.value.onUserSwitching(0, mock(IRemoteCallback::class.java))
+
+ var mostRecentSwitchingValue = false
+ underTest.userSwitchingInProgress.onEach { mostRecentSwitchingValue = it }.launchIn(this)
+
+ assertThat(mostRecentSwitchingValue).isTrue()
+
+ userSwitchObserver.value.onUserSwitchComplete(0)
+ assertThat(mostRecentSwitchingValue).isFalse()
+ }
+
private fun createUserInfo(
id: Int,
isGuest: Boolean,
@@ -280,6 +322,8 @@
}
private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(FACE_AUTH_REFACTOR, true)
return UserRepositoryImpl(
appContext = context,
manager = manager,
@@ -288,6 +332,8 @@
backgroundDispatcher = IMMEDIATE,
globalSettings = globalSettings,
tracker = tracker,
+ activityManager = activityManager,
+ featureFlags = featureFlags,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index 78b0cbe..9bb52be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -36,12 +36,14 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Text
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -60,12 +62,12 @@
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -73,10 +75,12 @@
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class UserInteractorTest : SysuiTestCase() {
@@ -90,10 +94,11 @@
@Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: UserInteractor
- private lateinit var testCoroutineScope: TestCoroutineScope
+ private lateinit var testScope: TestScope
private lateinit var userRepository: FakeUserRepository
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var telephonyRepository: FakeTelephonyRepository
@@ -117,11 +122,12 @@
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
telephonyRepository = FakeTelephonyRepository()
- testCoroutineScope = TestCoroutineScope()
+ val testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
val refreshUsersScheduler =
RefreshUsersScheduler(
- applicationScope = testCoroutineScope,
- mainDispatcher = IMMEDIATE,
+ applicationScope = testScope.backgroundScope,
+ mainDispatcher = testDispatcher,
repository = userRepository,
)
underTest =
@@ -132,23 +138,24 @@
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
+ commandQueue = commandQueue,
),
manager = manager,
- applicationScope = testCoroutineScope,
+ applicationScope = testScope.backgroundScope,
telephonyInteractor =
TelephonyInteractor(
repository = telephonyRepository,
),
broadcastDispatcher = fakeBroadcastDispatcher,
- backgroundDispatcher = IMMEDIATE,
+ backgroundDispatcher = testDispatcher,
activityManager = activityManager,
refreshUsersScheduler = refreshUsersScheduler,
guestUserInteractor =
GuestUserInteractor(
applicationContext = context,
- applicationScope = testCoroutineScope,
- mainDispatcher = IMMEDIATE,
- backgroundDispatcher = IMMEDIATE,
+ applicationScope = testScope.backgroundScope,
+ mainDispatcher = testDispatcher,
+ backgroundDispatcher = testDispatcher,
manager = manager,
repository = userRepository,
deviceProvisionedController = deviceProvisionedController,
@@ -164,7 +171,7 @@
@Test
fun `onRecordSelected - user`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -179,7 +186,7 @@
@Test
fun `onRecordSelected - switch to guest user`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -193,7 +200,7 @@
@Test
fun `onRecordSelected - enter guest mode`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -202,6 +209,7 @@
whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
+ runCurrent()
verify(dialogShower).dismiss()
verify(manager).createGuest(any())
@@ -210,7 +218,7 @@
@Test
fun `onRecordSelected - action`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -224,81 +232,72 @@
@Test
fun `users - switcher enabled`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var value: List<UserModel>? = null
- val job = underTest.users.onEach { value = it }.launchIn(this)
- assertUsers(models = value, count = 3, includeGuest = true)
+ val value = collectLastValue(underTest.users)
- job.cancel()
+ assertUsers(models = value(), count = 3, includeGuest = true)
}
@Test
fun `users - switches to second user`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var value: List<UserModel>? = null
- val job = underTest.users.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.users)
userRepository.setSelectedUserInfo(userInfos[1])
- assertUsers(models = value, count = 2, selectedIndex = 1)
- job.cancel()
+ assertUsers(models = value(), count = 2, selectedIndex = 1)
}
@Test
fun `users - switcher not enabled`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
- var value: List<UserModel>? = null
- val job = underTest.users.onEach { value = it }.launchIn(this)
- assertUsers(models = value, count = 1)
-
- job.cancel()
+ val value = collectLastValue(underTest.users)
+ assertUsers(models = value(), count = 1)
}
@Test
fun selectedUser() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var value: UserModel? = null
- val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
- assertUser(value, id = 0, isSelected = true)
+ val value = collectLastValue(underTest.selectedUser)
+ assertUser(value(), id = 0, isSelected = true)
userRepository.setSelectedUserInfo(userInfos[1])
- assertUser(value, id = 1, isSelected = true)
-
- job.cancel()
+ assertUser(value(), id = 1, isSelected = true)
}
@Test
fun `actions - device unlocked`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value)
+ runCurrent()
+
+ assertThat(value())
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
@@ -307,13 +306,11 @@
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
-
- job.cancel()
}
@Test
fun `actions - device unlocked - full screen`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
val userInfos = createUserInfos(count = 2, includeGuest = false)
@@ -321,10 +318,9 @@
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value)
+ assertThat(value())
.isEqualTo(
listOf(
UserActionModel.ADD_USER,
@@ -333,46 +329,38 @@
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
-
- job.cancel()
}
@Test
fun `actions - device unlocked user not primary - empty list`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
- job.cancel()
+ assertThat(value()).isEqualTo(emptyList<UserActionModel>())
}
@Test
fun `actions - device unlocked user is guest - empty list`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = true)
assertThat(userInfos[1].isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(false)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
- job.cancel()
+ assertThat(value()).isEqualTo(emptyList<UserActionModel>())
}
@Test
fun `actions - device locked add from lockscreen set - full list`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -383,10 +371,9 @@
)
)
keyguardRepository.setKeyguardShowing(false)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value)
+ assertThat(value())
.isEqualTo(
listOf(
UserActionModel.ENTER_GUEST_MODE,
@@ -395,13 +382,11 @@
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
-
- job.cancel()
}
@Test
fun `actions - device locked add from lockscreen set - full list - full screen`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
@@ -413,10 +398,9 @@
)
)
keyguardRepository.setKeyguardShowing(false)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value)
+ assertThat(value())
.isEqualTo(
listOf(
UserActionModel.ADD_USER,
@@ -425,39 +409,33 @@
UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
)
)
-
- job.cancel()
}
@Test
fun `actions - device locked - only manage user is shown`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
keyguardRepository.setKeyguardShowing(true)
- var value: List<UserActionModel>? = null
- val job = underTest.actions.onEach { value = it }.launchIn(this)
+ val value = collectLastValue(underTest.actions)
- assertThat(value).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
-
- job.cancel()
+ assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
}
@Test
fun `executeAction - add user - dialog shown`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
- var dialogRequest: ShowDialogRequestModel? = null
- val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+ val dialogRequest = collectLastValue(underTest.dialogShowRequests)
val dialogShower: UserSwitchDialogController.DialogShower = mock()
underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
- assertThat(dialogRequest)
+ assertThat(dialogRequest())
.isEqualTo(
ShowDialogRequestModel.ShowAddUserDialog(
userHandle = userInfos[0].userHandle,
@@ -468,14 +446,12 @@
)
underTest.onDialogShown()
- assertThat(dialogRequest).isNull()
-
- job.cancel()
+ assertThat(dialogRequest()).isNull()
}
@Test
- fun `executeAction - add supervised user - starts activity`() =
- runBlocking(IMMEDIATE) {
+ fun `executeAction - add supervised user - dismisses dialog and starts activity`() =
+ testScope.runTest {
underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
val intentCaptor = kotlinArgumentCaptor<Intent>()
@@ -487,7 +463,7 @@
@Test
fun `executeAction - navigate to manage users`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
val intentCaptor = kotlinArgumentCaptor<Intent>()
@@ -497,7 +473,7 @@
@Test
fun `executeAction - guest mode`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -505,110 +481,103 @@
val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
- val showDialogsJob =
- underTest.dialogShowRequests
- .onEach {
- dialogRequests.add(it)
- if (it != null) {
- underTest.onDialogShown()
- }
+ backgroundScope.launch {
+ underTest.dialogShowRequests.collect {
+ dialogRequests.add(it)
+ if (it != null) {
+ underTest.onDialogShown()
}
- .launchIn(this)
- val dismissDialogsJob =
- underTest.dialogDismissRequests
- .onEach {
- if (it != null) {
- underTest.onDialogDismissed()
- }
+ }
+ }
+ backgroundScope.launch {
+ underTest.dialogDismissRequests.collect {
+ if (it != null) {
+ underTest.onDialogDismissed()
}
- .launchIn(this)
+ }
+ }
underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+ runCurrent()
assertThat(dialogRequests)
.contains(
ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
)
verify(activityManager).switchUser(guestUserInfo.id)
-
- showDialogsJob.cancel()
- dismissDialogsJob.cancel()
}
@Test
fun `selectUser - already selected guest re-selected - exit guest dialog`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = true)
val guestUserInfo = userInfos[1]
assertThat(guestUserInfo.isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(guestUserInfo)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var dialogRequest: ShowDialogRequestModel? = null
- val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+ val dialogRequest = collectLastValue(underTest.dialogShowRequests)
underTest.selectUser(
newlySelectedUserId = guestUserInfo.id,
dialogShower = dialogShower,
)
- assertThat(dialogRequest)
+ assertThat(dialogRequest())
.isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
verify(dialogShower, never()).dismiss()
- job.cancel()
}
@Test
fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = true)
val guestUserInfo = userInfos[1]
assertThat(guestUserInfo.isGuest).isTrue()
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(guestUserInfo)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var dialogRequest: ShowDialogRequestModel? = null
- val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+ val dialogRequest = collectLastValue(underTest.dialogShowRequests)
underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
- assertThat(dialogRequest)
+ assertThat(dialogRequest())
.isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
verify(dialogShower, never()).dismiss()
- job.cancel()
}
@Test
fun `selectUser - not currently guest - switches users`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var dialogRequest: ShowDialogRequestModel? = null
- val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+ val dialogRequest = collectLastValue(underTest.dialogShowRequests)
underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
- assertThat(dialogRequest).isNull()
+ assertThat(dialogRequest()).isNull()
verify(activityManager).switchUser(userInfos[1].id)
verify(dialogShower).dismiss()
- job.cancel()
}
@Test
fun `Telephony call state changes - refreshes users`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
+ runCurrent()
+
val refreshUsersCallCount = userRepository.refreshUsersCallCount
telephonyRepository.setCallState(1)
+ runCurrent()
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `User switched broadcast`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -617,9 +586,11 @@
val callback2: UserInteractor.UserCallback = mock()
underTest.addCallback(callback1)
underTest.addCallback(callback2)
+ runCurrent()
val refreshUsersCallCount = userRepository.refreshUsersCallCount
userRepository.setSelectedUserInfo(userInfos[1])
+ runCurrent()
fakeBroadcastDispatcher.registeredReceivers.forEach {
it.onReceive(
context,
@@ -627,16 +598,17 @@
.putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
)
}
+ runCurrent()
- verify(callback1).onUserStateChanged()
- verify(callback2).onUserStateChanged()
+ verify(callback1, atLeastOnce()).onUserStateChanged()
+ verify(callback2, atLeastOnce()).onUserStateChanged()
assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `User info changed broadcast`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -649,12 +621,14 @@
)
}
+ runCurrent()
+
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `System user unlocked broadcast - refresh users`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -667,13 +641,14 @@
.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
)
}
+ runCurrent()
assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
}
@Test
fun `Non-system user unlocked broadcast - do not refresh users`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
@@ -691,14 +666,14 @@
@Test
fun userRecords() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
- testCoroutineScope.advanceUntilIdle()
+ runCurrent()
assertRecords(
records = underTest.userRecords.value,
@@ -717,7 +692,7 @@
@Test
fun userRecordsFullScreen() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
val userInfos = createUserInfos(count = 3, includeGuest = false)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
@@ -725,7 +700,7 @@
userRepository.setSelectedUserInfo(userInfos[0])
keyguardRepository.setKeyguardShowing(false)
- testCoroutineScope.advanceUntilIdle()
+ runCurrent()
assertRecords(
records = underTest.userRecords.value,
@@ -744,7 +719,7 @@
@Test
fun selectedUserRecord() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
userRepository.setUserInfos(userInfos)
@@ -762,63 +737,54 @@
@Test
fun `users - secondary user - guest user can be switched to`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var res: List<UserModel>? = null
- val job = underTest.users.onEach { res = it }.launchIn(this)
- assertThat(res?.size == 3).isTrue()
- assertThat(res?.find { it.isGuest }).isNotNull()
- job.cancel()
+ val res = collectLastValue(underTest.users)
+ assertThat(res()?.size == 3).isTrue()
+ assertThat(res()?.find { it.isGuest }).isNotNull()
}
@Test
fun `users - secondary user - no guest action`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var res: List<UserActionModel>? = null
- val job = underTest.actions.onEach { res = it }.launchIn(this)
- assertThat(res?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
- job.cancel()
+ val res = collectLastValue(underTest.actions)
+ assertThat(res()?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
}
@Test
fun `users - secondary user - no guest user record`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 3, includeGuest = true)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var res: List<UserRecord>? = null
- val job = underTest.userRecords.onEach { res = it }.launchIn(this)
- assertThat(res?.find { it.isGuest }).isNull()
- job.cancel()
+ assertThat(underTest.userRecords.value.find { it.isGuest }).isNull()
}
@Test
fun `show user switcher - full screen disabled - shows dialog switcher`() =
- runBlocking(IMMEDIATE) {
- var dialogRequest: ShowDialogRequestModel? = null
+ testScope.runTest {
val expandable = mock<Expandable>()
underTest.showUserSwitcher(context, expandable)
- val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+ val dialogRequest = collectLastValue(underTest.dialogShowRequests)
// Dialog is shown.
- assertThat(dialogRequest).isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog)
+ assertThat(dialogRequest())
+ .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable))
underTest.onDialogShown()
- assertThat(dialogRequest).isNull()
-
- job.cancel()
+ assertThat(dialogRequest()).isNull()
}
@Test
@@ -849,8 +815,8 @@
@Test
fun `users - secondary user - managed profile is not included`() =
- runBlocking(IMMEDIATE) {
- var userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
+ testScope.runTest {
+ val userInfos = createUserInfos(count = 3, includeGuest = false).toMutableList()
userInfos.add(
UserInfo(
50,
@@ -863,23 +829,19 @@
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
- var res: List<UserModel>? = null
- val job = underTest.users.onEach { res = it }.launchIn(this)
- assertThat(res?.size == 3).isTrue()
- job.cancel()
+ val res = collectLastValue(underTest.users)
+ assertThat(res()?.size == 3).isTrue()
}
@Test
fun `current user is not primary and user switcher is disabled`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val userInfos = createUserInfos(count = 2, includeGuest = false)
userRepository.setUserInfos(userInfos)
userRepository.setSelectedUserInfo(userInfos[1])
userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
- var selectedUser: UserModel? = null
- val job = underTest.selectedUser.onEach { selectedUser = it }.launchIn(this)
- assertThat(selectedUser).isNotNull()
- job.cancel()
+ val selectedUser = collectLastValue(underTest.selectedUser)
+ assertThat(selectedUser()).isNotNull()
}
private fun assertUsers(
@@ -1017,7 +979,6 @@
}
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
private val GUEST_ICON: Drawable = mock()
private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 108fa62..9a4ca56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -75,6 +76,7 @@
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: StatusBarUserChipViewModel
@@ -241,6 +243,7 @@
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
+ commandQueue = commandQueue,
),
featureFlags =
FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) },
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 784a26b..3d4bbdb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -76,6 +77,7 @@
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
@Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+ @Mock private lateinit var commandQueue: CommandQueue
private lateinit var underTest: UserSwitcherViewModel
@@ -142,6 +144,7 @@
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
+ commandQueue = commandQueue,
),
featureFlags =
FakeFeatureFlags().apply {
@@ -169,273 +172,295 @@
}
@Test
- fun users() = testScope.runTest {
- val userInfos =
- listOf(
- UserInfo(
- /* id= */ 0,
- /* name= */ "zero",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_SYSTEM,
- ),
- UserInfo(
- /* id= */ 1,
- /* name= */ "one",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_SYSTEM,
- ),
- UserInfo(
- /* id= */ 2,
- /* name= */ "two",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_SYSTEM,
- ),
- )
- userRepository.setUserInfos(userInfos)
- userRepository.setSelectedUserInfo(userInfos[0])
-
- val userViewModels = mutableListOf<List<UserViewModel>>()
- val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
-
- assertThat(userViewModels.last()).hasSize(3)
- assertUserViewModel(
- viewModel = userViewModels.last()[0],
- viewKey = 0,
- name = Text.Loaded("zero"),
- isSelectionMarkerVisible = true,
- )
- assertUserViewModel(
- viewModel = userViewModels.last()[1],
- viewKey = 1,
- name = Text.Loaded("one"),
- isSelectionMarkerVisible = false,
- )
- assertUserViewModel(
- viewModel = userViewModels.last()[2],
- viewKey = 2,
- name = Text.Loaded("two"),
- isSelectionMarkerVisible = false,
- )
- job.cancel()
- }
-
- @Test
- fun `maximumUserColumns - few users`() = testScope.runTest {
- setUsers(count = 2)
- val values = mutableListOf<Int>()
- val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
-
- assertThat(values.last()).isEqualTo(4)
-
- job.cancel()
- }
-
- @Test
- fun `maximumUserColumns - many users`() = testScope.runTest {
- setUsers(count = 5)
- val values = mutableListOf<Int>()
- val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
-
- assertThat(values.last()).isEqualTo(3)
- job.cancel()
- }
-
- @Test
- fun `isOpenMenuButtonVisible - has actions - true`() = testScope.runTest {
- setUsers(2)
-
- val isVisible = mutableListOf<Boolean>()
- val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
-
- assertThat(isVisible.last()).isTrue()
- job.cancel()
- }
-
- @Test
- fun `isOpenMenuButtonVisible - no actions - false`() = testScope.runTest {
- val userInfos = setUsers(2)
- userRepository.setSelectedUserInfo(userInfos[1])
- keyguardRepository.setKeyguardShowing(true)
- whenever(manager.canAddMoreUsers(any())).thenReturn(false)
-
- val isVisible = mutableListOf<Boolean>()
- val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
-
- assertThat(isVisible.last()).isFalse()
- job.cancel()
- }
-
- @Test
- fun menu() = testScope.runTest {
- val isMenuVisible = mutableListOf<Boolean>()
- val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) }
- assertThat(isMenuVisible.last()).isFalse()
-
- underTest.onOpenMenuButtonClicked()
- assertThat(isMenuVisible.last()).isTrue()
-
- underTest.onMenuClosed()
- assertThat(isMenuVisible.last()).isFalse()
-
- job.cancel()
- }
-
- @Test
- fun `menu actions`() = testScope.runTest {
- setUsers(2)
- val actions = mutableListOf<List<UserActionViewModel>>()
- val job = launch(testDispatcher) { underTest.menu.toList(actions) }
-
- assertThat(actions.last().map { it.viewKey })
- .isEqualTo(
+ fun users() =
+ testScope.runTest {
+ val userInfos =
listOf(
- UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
- UserActionModel.ADD_USER.ordinal.toLong(),
- UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+ UserInfo(
+ /* id= */ 0,
+ /* name= */ "zero",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_PRIMARY or
+ UserInfo.FLAG_ADMIN or
+ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_SYSTEM,
+ ),
+ UserInfo(
+ /* id= */ 1,
+ /* name= */ "one",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_SYSTEM,
+ ),
+ UserInfo(
+ /* id= */ 2,
+ /* name= */ "two",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_SYSTEM,
+ ),
)
- )
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
- job.cancel()
- }
+ val userViewModels = mutableListOf<List<UserViewModel>>()
+ val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
- @Test
- fun `isFinishRequested - finishes when user is switched`() = testScope.runTest {
- val userInfos = setUsers(count = 2)
- val isFinishRequested = mutableListOf<Boolean>()
- val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
- assertThat(isFinishRequested.last()).isFalse()
-
- userRepository.setSelectedUserInfo(userInfos[1])
-
- assertThat(isFinishRequested.last()).isTrue()
-
- job.cancel()
- }
-
- @Test
- fun `isFinishRequested - finishes when the screen turns off`() = testScope.runTest {
- setUsers(count = 2)
- powerRepository.setInteractive(true)
- val isFinishRequested = mutableListOf<Boolean>()
- val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
- assertThat(isFinishRequested.last()).isFalse()
-
- powerRepository.setInteractive(false)
-
- assertThat(isFinishRequested.last()).isTrue()
-
- job.cancel()
- }
-
- @Test
- fun `isFinishRequested - finishes when cancel button is clicked`() = testScope.runTest {
- setUsers(count = 2)
- powerRepository.setInteractive(true)
- val isFinishRequested = mutableListOf<Boolean>()
- val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
- assertThat(isFinishRequested.last()).isFalse()
-
- underTest.onCancelButtonClicked()
-
- assertThat(isFinishRequested.last()).isTrue()
-
- underTest.onFinished()
-
- assertThat(isFinishRequested.last()).isFalse()
-
- job.cancel()
- }
-
- @Test
- fun `guest selected -- name is exit guest`() = testScope.runTest {
- val userInfos =
- listOf(
- UserInfo(
- /* id= */ 0,
- /* name= */ "zero",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_SYSTEM,
- ),
- UserInfo(
- /* id= */ 1,
- /* name= */ "one",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_GUEST,
- ),
- )
-
- userRepository.setUserInfos(userInfos)
- userRepository.setSelectedUserInfo(userInfos[1])
-
- val userViewModels = mutableListOf<List<UserViewModel>>()
- val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
-
- assertThat(userViewModels.last()).hasSize(2)
- assertUserViewModel(
- viewModel = userViewModels.last()[0],
- viewKey = 0,
- name = Text.Loaded("zero"),
- isSelectionMarkerVisible = false,
- )
- assertUserViewModel(
- viewModel = userViewModels.last()[1],
- viewKey = 1,
- name = Text.Resource(
- com.android.settingslib.R.string.guest_exit_quick_settings_button
- ),
- isSelectionMarkerVisible = true,
- )
- job.cancel()
- }
-
- @Test
- fun `guest not selected -- name is guest`() = testScope.runTest {
- val userInfos =
- listOf(
- UserInfo(
- /* id= */ 0,
- /* name= */ "zero",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_SYSTEM,
- ),
- UserInfo(
- /* id= */ 1,
- /* name= */ "one",
- /* iconPath= */ "",
- /* flags= */ UserInfo.FLAG_FULL,
- UserManager.USER_TYPE_FULL_GUEST,
- ),
- )
-
- userRepository.setUserInfos(userInfos)
- userRepository.setSelectedUserInfo(userInfos[0])
- runCurrent()
-
- val userViewModels = mutableListOf<List<UserViewModel>>()
- val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
-
- assertThat(userViewModels.last()).hasSize(2)
- assertUserViewModel(
+ assertThat(userViewModels.last()).hasSize(3)
+ assertUserViewModel(
viewModel = userViewModels.last()[0],
viewKey = 0,
name = Text.Loaded("zero"),
isSelectionMarkerVisible = true,
- )
- assertUserViewModel(
+ )
+ assertUserViewModel(
viewModel = userViewModels.last()[1],
viewKey = 1,
name = Text.Loaded("one"),
isSelectionMarkerVisible = false,
- )
- job.cancel()
- }
+ )
+ assertUserViewModel(
+ viewModel = userViewModels.last()[2],
+ viewKey = 2,
+ name = Text.Loaded("two"),
+ isSelectionMarkerVisible = false,
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `maximumUserColumns - few users`() =
+ testScope.runTest {
+ setUsers(count = 2)
+ val values = mutableListOf<Int>()
+ val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
+
+ assertThat(values.last()).isEqualTo(4)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `maximumUserColumns - many users`() =
+ testScope.runTest {
+ setUsers(count = 5)
+ val values = mutableListOf<Int>()
+ val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
+
+ assertThat(values.last()).isEqualTo(3)
+ job.cancel()
+ }
+
+ @Test
+ fun `isOpenMenuButtonVisible - has actions - true`() =
+ testScope.runTest {
+ setUsers(2)
+
+ val isVisible = mutableListOf<Boolean>()
+ val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
+
+ assertThat(isVisible.last()).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun `isOpenMenuButtonVisible - no actions - false`() =
+ testScope.runTest {
+ val userInfos = setUsers(2)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ keyguardRepository.setKeyguardShowing(true)
+ whenever(manager.canAddMoreUsers(any())).thenReturn(false)
+
+ val isVisible = mutableListOf<Boolean>()
+ val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
+
+ assertThat(isVisible.last()).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun menu() =
+ testScope.runTest {
+ val isMenuVisible = mutableListOf<Boolean>()
+ val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) }
+ assertThat(isMenuVisible.last()).isFalse()
+
+ underTest.onOpenMenuButtonClicked()
+ assertThat(isMenuVisible.last()).isTrue()
+
+ underTest.onMenuClosed()
+ assertThat(isMenuVisible.last()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `menu actions`() =
+ testScope.runTest {
+ setUsers(2)
+ val actions = mutableListOf<List<UserActionViewModel>>()
+ val job = launch(testDispatcher) { underTest.menu.toList(actions) }
+
+ assertThat(actions.last().map { it.viewKey })
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+ UserActionModel.ADD_USER.ordinal.toLong(),
+ UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFinishRequested - finishes when user is switched`() =
+ testScope.runTest {
+ val userInfos = setUsers(count = 2)
+ val isFinishRequested = mutableListOf<Boolean>()
+ val job =
+ launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+ assertThat(isFinishRequested.last()).isFalse()
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+
+ assertThat(isFinishRequested.last()).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFinishRequested - finishes when the screen turns off`() =
+ testScope.runTest {
+ setUsers(count = 2)
+ powerRepository.setInteractive(true)
+ val isFinishRequested = mutableListOf<Boolean>()
+ val job =
+ launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+ assertThat(isFinishRequested.last()).isFalse()
+
+ powerRepository.setInteractive(false)
+
+ assertThat(isFinishRequested.last()).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `isFinishRequested - finishes when cancel button is clicked`() =
+ testScope.runTest {
+ setUsers(count = 2)
+ powerRepository.setInteractive(true)
+ val isFinishRequested = mutableListOf<Boolean>()
+ val job =
+ launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+ assertThat(isFinishRequested.last()).isFalse()
+
+ underTest.onCancelButtonClicked()
+
+ assertThat(isFinishRequested.last()).isTrue()
+
+ underTest.onFinished()
+
+ assertThat(isFinishRequested.last()).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `guest selected -- name is exit guest`() =
+ testScope.runTest {
+ val userInfos =
+ listOf(
+ UserInfo(
+ /* id= */ 0,
+ /* name= */ "zero",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_PRIMARY or
+ UserInfo.FLAG_ADMIN or
+ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_SYSTEM,
+ ),
+ UserInfo(
+ /* id= */ 1,
+ /* name= */ "one",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_GUEST,
+ ),
+ )
+
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+
+ val userViewModels = mutableListOf<List<UserViewModel>>()
+ val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
+
+ assertThat(userViewModels.last()).hasSize(2)
+ assertUserViewModel(
+ viewModel = userViewModels.last()[0],
+ viewKey = 0,
+ name = Text.Loaded("zero"),
+ isSelectionMarkerVisible = false,
+ )
+ assertUserViewModel(
+ viewModel = userViewModels.last()[1],
+ viewKey = 1,
+ name =
+ Text.Resource(
+ com.android.settingslib.R.string.guest_exit_quick_settings_button
+ ),
+ isSelectionMarkerVisible = true,
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `guest not selected -- name is guest`() =
+ testScope.runTest {
+ val userInfos =
+ listOf(
+ UserInfo(
+ /* id= */ 0,
+ /* name= */ "zero",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_PRIMARY or
+ UserInfo.FLAG_ADMIN or
+ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_SYSTEM,
+ ),
+ UserInfo(
+ /* id= */ 1,
+ /* name= */ "one",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_GUEST,
+ ),
+ )
+
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ runCurrent()
+
+ val userViewModels = mutableListOf<List<UserViewModel>>()
+ val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
+
+ assertThat(userViewModels.last()).hasSize(2)
+ assertUserViewModel(
+ viewModel = userViewModels.last()[0],
+ viewKey = 0,
+ name = Text.Loaded("zero"),
+ isSelectionMarkerVisible = true,
+ )
+ assertUserViewModel(
+ viewModel = userViewModels.last()[1],
+ viewKey = 1,
+ name = Text.Loaded("one"),
+ isSelectionMarkerVisible = false,
+ )
+ job.cancel()
+ }
private suspend fun setUsers(count: Int): List<UserInfo> {
val userInfos =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
index 6bfc2f1..7d0d57b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
@@ -19,9 +19,12 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -35,6 +38,10 @@
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import org.junit.Test
import org.junit.runner.RunWith
@@ -231,6 +238,170 @@
}
}
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ThrottleFlowTest : SysuiTestCase() {
+
+ @Test
+ fun doesNotAffectEmissions_whenDelayAtLeastEqualToPeriod() = runTest {
+ // Arrange
+ val choreographer = createChoreographer(this)
+ val output = mutableListOf<Int>()
+ val collectJob = backgroundScope.launch {
+ flow {
+ emit(1)
+ delay(1000)
+ emit(2)
+ }.throttle(1000, choreographer.fakeClock).toList(output)
+ }
+
+ // Act
+ choreographer.advanceAndRun(0)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(999)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(1)
+
+ // Assert
+ assertThat(output).containsExactly(1, 2)
+
+ // Cleanup
+ collectJob.cancel()
+ }
+
+ @Test
+ fun delaysEmissions_withShorterThanPeriodDelay_untilPeriodElapses() = runTest {
+ // Arrange
+ val choreographer = createChoreographer(this)
+ val output = mutableListOf<Int>()
+ val collectJob = backgroundScope.launch {
+ flow {
+ emit(1)
+ delay(500)
+ emit(2)
+ }.throttle(1000, choreographer.fakeClock).toList(output)
+ }
+
+ // Act
+ choreographer.advanceAndRun(0)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(500)
+ choreographer.advanceAndRun(499)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(1)
+
+ // Assert
+ assertThat(output).containsExactly(1, 2)
+
+ // Cleanup
+ collectJob.cancel()
+ }
+
+ @Test
+ fun filtersAllButLastEmission_whenMultipleEmissionsInPeriod() = runTest {
+ // Arrange
+ val choreographer = createChoreographer(this)
+ val output = mutableListOf<Int>()
+ val collectJob = backgroundScope.launch {
+ flow {
+ emit(1)
+ delay(500)
+ emit(2)
+ delay(500)
+ emit(3)
+ }.throttle(1000, choreographer.fakeClock).toList(output)
+ }
+
+ // Act
+ choreographer.advanceAndRun(0)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(500)
+ choreographer.advanceAndRun(499)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(1)
+
+ // Assert
+ assertThat(output).containsExactly(1, 3)
+
+ // Cleanup
+ collectJob.cancel()
+ }
+
+ @Test
+ fun filtersAllButLastEmission_andDelaysIt_whenMultipleEmissionsInShorterThanPeriod() = runTest {
+ // Arrange
+ val choreographer = createChoreographer(this)
+ val output = mutableListOf<Int>()
+ val collectJob = backgroundScope.launch {
+ flow {
+ emit(1)
+ delay(500)
+ emit(2)
+ delay(250)
+ emit(3)
+ }.throttle(1000, choreographer.fakeClock).toList(output)
+ }
+
+ // Act
+ choreographer.advanceAndRun(0)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(500)
+ choreographer.advanceAndRun(250)
+ choreographer.advanceAndRun(249)
+
+ // Assert
+ assertThat(output).containsExactly(1)
+
+ // Act
+ choreographer.advanceAndRun(1)
+
+ // Assert
+ assertThat(output).containsExactly(1, 3)
+
+ // Cleanup
+ collectJob.cancel()
+ }
+
+ private fun createChoreographer(testScope: TestScope) = object {
+ val fakeClock = FakeSystemClock()
+
+ fun advanceAndRun(millis: Long) {
+ fakeClock.advanceTime(millis)
+ testScope.advanceTimeBy(millis)
+ testScope.runCurrent()
+ }
+ }
+}
+
private fun <T> assertThatFlow(flow: Flow<T>) =
object {
suspend fun emitsExactly(vararg emissions: T) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
index d0420f7..729168a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakReporterTest.java
@@ -22,13 +22,17 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.NotificationManager;
+import android.os.UserHandle;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import org.junit.After;
import org.junit.Before;
@@ -48,6 +52,7 @@
private File mLeakDir;
private File mLeakDump;
private File mLeakHprof;
+ private UserTracker mUserTracker;
private NotificationManager mNotificationManager;
@Before
@@ -56,6 +61,9 @@
mLeakDump = new File(mLeakDir, LeakReporter.LEAK_DUMP);
mLeakHprof = new File(mLeakDir, LeakReporter.LEAK_HPROF);
+ mUserTracker = mock(UserTracker.class);
+ when(mUserTracker.getUserHandle()).thenReturn(
+ UserHandle.of(ActivityManager.getCurrentUser()));
mNotificationManager = mock(NotificationManager.class);
mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
@@ -65,7 +73,7 @@
return null;
}).when(mLeakDetector).dump(any(), any());
- mLeakReporter = new LeakReporter(mContext, mLeakDetector, "test@example.com");
+ mLeakReporter = new LeakReporter(mContext, mUserTracker, mLeakDetector, "test@example.com");
}
@After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index 915ea1a..0663004 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -48,6 +48,7 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.util.RingerModeLiveData;
import com.android.systemui.util.RingerModeTracker;
@@ -101,6 +102,8 @@
@Mock
private ActivityManager mActivityManager;
@Mock
+ private UserTracker mUserTracker;
+ @Mock
private DumpManager mDumpManager;
@@ -113,6 +116,7 @@
// Initial non-set value
when(mRingerModeLiveData.getValue()).thenReturn(-1);
when(mRingerModeInternalLiveData.getValue()).thenReturn(-1);
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
// Enable group volume adjustments
mContext.getOrCreateTestableResources().addOverride(
com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions,
@@ -124,7 +128,7 @@
mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager,
mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager,
mPackageManager, mWakefullnessLifcycle, mCaptioningManager, mKeyguardManager,
- mActivityManager, mDumpManager, mCallback);
+ mActivityManager, mUserTracker, mDumpManager, mCallback);
mVolumeController.setEnableDialogs(true, true);
}
@@ -233,12 +237,13 @@
CaptioningManager captioningManager,
KeyguardManager keyguardManager,
ActivityManager activityManager,
+ UserTracker userTracker,
DumpManager dumpManager,
C callback) {
super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager,
notificationManager, optionalVibrator, iAudioService, accessibilityManager,
packageManager, wakefulnessLifecycle, captioningManager, keyguardManager,
- activityManager, dumpManager);
+ activityManager, userTracker, dumpManager);
mCallbacks = callback;
ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index c3c6975..d419095 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.volume;
+import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
import static junit.framework.Assert.assertEquals;
@@ -342,6 +343,15 @@
assertEquals(mDialog.mVolumeRingerMuteIconDrawableId, R.drawable.ic_volume_ringer_mute);
}
+ @Test
+ public void testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation() {
+ mDialog.dismissH(DISMISS_REASON_UNKNOWN);
+ // notifyVisible(false) should not be called immediately but only after the dismiss
+ // animation has ended.
+ verify(mVolumeDialogController, times(0)).notifyVisible(false);
+ mDialog.getDialogView().animate().cancel();
+ }
+
/*
@Test
public void testContentDescriptions() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
new file mode 100644
index 0000000..b527861
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsControllerTest.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.wallet.controller
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.service.quickaccesswallet.WalletCard
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import java.util.ArrayList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class WalletContextualSuggestionsControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var walletController: QuickAccessWalletController
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var featureFlags: FeatureFlags
+ @Mock private lateinit var mockContext: Context
+ @Captor private lateinit var broadcastReceiver: ArgumentCaptor<BroadcastReceiver>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(
+ broadcastDispatcher.broadcastFlow<List<String>?>(
+ any(),
+ isNull(),
+ any(),
+ any(),
+ any()
+ )
+ )
+ .thenCallRealMethod()
+
+ whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
+ .thenReturn(true)
+
+ whenever(CARD_1.cardId).thenReturn(ID_1)
+ whenever(CARD_2.cardId).thenReturn(ID_2)
+ whenever(CARD_3.cardId).thenReturn(ID_3)
+ }
+
+ @Test
+ fun `state - has wallet cards - received contextual cards`() = runTest {
+ setUpWalletClient(listOf(CARD_1, CARD_2))
+ val latest =
+ collectLastValue(
+ createWalletContextualSuggestionsController(backgroundScope)
+ .contextualSuggestionCards,
+ )
+
+ runCurrent()
+ verifyRegistered()
+ broadcastReceiver.value.onReceive(
+ mockContext,
+ createContextualCardsIntent(listOf(ID_1, ID_2))
+ )
+
+ assertThat(latest()).containsExactly(CARD_1, CARD_2)
+ }
+
+ @Test
+ fun `state - no wallet cards - received contextual cards`() = runTest {
+ setUpWalletClient(emptyList())
+ val latest =
+ collectLastValue(
+ createWalletContextualSuggestionsController(backgroundScope)
+ .contextualSuggestionCards,
+ )
+
+ runCurrent()
+ verifyRegistered()
+ broadcastReceiver.value.onReceive(
+ mockContext,
+ createContextualCardsIntent(listOf(ID_1, ID_2))
+ )
+
+ assertThat(latest()).isEmpty()
+ }
+
+ @Test
+ fun `state - has wallet cards - no contextual cards`() = runTest {
+ setUpWalletClient(listOf(CARD_1, CARD_2))
+ val latest =
+ collectLastValue(
+ createWalletContextualSuggestionsController(backgroundScope)
+ .contextualSuggestionCards,
+ )
+
+ runCurrent()
+ verifyRegistered()
+ broadcastReceiver.value.onReceive(mockContext, createContextualCardsIntent(emptyList()))
+
+ assertThat(latest()).isEmpty()
+ }
+
+ @Test
+ fun `state - wallet cards error`() = runTest {
+ setUpWalletClient(shouldFail = true)
+ val latest =
+ collectLastValue(
+ createWalletContextualSuggestionsController(backgroundScope)
+ .contextualSuggestionCards,
+ )
+
+ runCurrent()
+ verifyRegistered()
+ broadcastReceiver.value.onReceive(
+ mockContext,
+ createContextualCardsIntent(listOf(ID_1, ID_2))
+ )
+
+ assertThat(latest()).isEmpty()
+ }
+
+ @Test
+ fun `state - no contextual cards extra`() = runTest {
+ setUpWalletClient(listOf(CARD_1, CARD_2))
+ val latest =
+ collectLastValue(
+ createWalletContextualSuggestionsController(backgroundScope)
+ .contextualSuggestionCards,
+ )
+
+ runCurrent()
+ verifyRegistered()
+ broadcastReceiver.value.onReceive(mockContext, Intent(INTENT_NAME))
+
+ assertThat(latest()).isEmpty()
+ }
+
+ @Test
+ fun `state - has wallet cards - received contextual cards - feature disabled`() = runTest {
+ whenever(featureFlags.isEnabled(eq(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)))
+ .thenReturn(false)
+ setUpWalletClient(listOf(CARD_1, CARD_2))
+ val latest =
+ collectLastValue(
+ createWalletContextualSuggestionsController(backgroundScope)
+ .contextualSuggestionCards,
+ )
+
+ runCurrent()
+ verify(broadcastDispatcher, never()).broadcastFlow(any(), isNull(), any(), any())
+ assertThat(latest()).isNull()
+ }
+
+ private fun createWalletContextualSuggestionsController(
+ scope: CoroutineScope
+ ): WalletContextualSuggestionsController {
+ return WalletContextualSuggestionsController(
+ scope,
+ walletController,
+ broadcastDispatcher,
+ featureFlags
+ )
+ }
+
+ private fun verifyRegistered() {
+ verify(broadcastDispatcher)
+ .registerReceiver(capture(broadcastReceiver), any(), isNull(), isNull(), any(), any())
+ }
+
+ private fun createContextualCardsIntent(
+ ids: List<String> = emptyList(),
+ ): Intent {
+ val intent = Intent(INTENT_NAME)
+ intent.putStringArrayListExtra("cardIds", ArrayList(ids))
+ return intent
+ }
+
+ private fun setUpWalletClient(
+ cards: List<WalletCard> = emptyList(),
+ shouldFail: Boolean = false
+ ) {
+ whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
+ with(
+ invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback
+ ) {
+ if (shouldFail) {
+ onWalletCardRetrievalError(mock())
+ } else {
+ onWalletCardsRetrieved(GetWalletCardsResponse(cards, 0))
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val ID_1: String = "123"
+ private val CARD_1: WalletCard = mock()
+ private const val ID_2: String = "456"
+ private val CARD_2: WalletCard = mock()
+ private const val ID_3: String = "789"
+ private val CARD_3: WalletCard = mock()
+ private val INTENT_NAME: String = "WalletSuggestionsIntent"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
index 0fdcb95..31cce4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
@@ -32,13 +32,13 @@
import static org.mockito.Mockito.when;
import static org.mockito.hamcrest.MockitoHamcrest.intThat;
+import android.app.ActivityManager;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.ColorSpace;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
-import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Surface;
@@ -49,6 +49,7 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -81,6 +82,8 @@
private Surface mSurface;
@Mock
private Context mMockContext;
+ @Mock
+ private UserTracker mUserTracker;
@Mock
private Bitmap mWallpaperBitmap;
@@ -108,13 +111,16 @@
when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888);
// set up wallpaper manager
- when(mWallpaperManager.getBitmapAsUser(eq(UserHandle.USER_CURRENT), anyBoolean()))
+ when(mWallpaperManager.getBitmapAsUser(eq(ActivityManager.getCurrentUser()), anyBoolean()))
.thenReturn(mWallpaperBitmap);
when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager);
// set up surface
when(mSurfaceHolder.getSurface()).thenReturn(mSurface);
doNothing().when(mSurface).hwuiDestroy();
+
+ // set up UserTracker
+ when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
}
@Test
@@ -170,7 +176,7 @@
}
private ImageWallpaper createImageWallpaper() {
- return new ImageWallpaper(mFakeBackgroundExecutor) {
+ return new ImageWallpaper(mFakeBackgroundExecutor, mUserTracker) {
@Override
public Engine onCreateEngine() {
return new CanvasEngine() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 388c51f..bc33439 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -24,6 +24,7 @@
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
import static com.google.common.truth.Truth.assertThat;
@@ -90,6 +91,8 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.FakeDisplayTracker;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.ShadeController;
@@ -228,6 +231,8 @@
private BubbleEntry mBubbleEntryUser11;
private BubbleEntry mBubbleEntry2User11;
+ private Intent mAppBubbleIntent;
+
@Mock
private ShellInit mShellInit;
@Mock
@@ -283,6 +288,8 @@
private TestableLooper mTestableLooper;
+ private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -323,10 +330,13 @@
mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry(
mNotificationTestHelper.createBubble(handle));
+ mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
+ mAppBubbleIntent.setPackage(mContext.getPackageName());
+
mZenModeConfig.suppressedVisualEffects = 0;
when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
- mSysUiState = new SysUiState();
+ mSysUiState = new SysUiState(mDisplayTracker);
mSysUiState.addCallback(sysUiFlags -> {
mSysUiStateBubblesManageMenuExpanded =
(sysUiFlags
@@ -355,7 +365,8 @@
mock(Handler.class),
mock(NotifPipelineFlags.class),
mock(KeyguardNotificationVisibilityProvider.class),
- mock(UiEventLogger.class)
+ mock(UiEventLogger.class),
+ mock(UserTracker.class)
);
when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
mBubbleController = new TestableBubbleController(
@@ -1630,6 +1641,62 @@
any(Bubble.class), anyBoolean(), anyBoolean());
}
+ @Test
+ public void testShowOrHideAppBubble_addsAndExpand() {
+ assertThat(mBubbleController.isStackExpanded()).isFalse();
+ assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull();
+
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+
+ verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true),
+ /* showInShade= */ eq(false));
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+ assertThat(mBubbleController.isStackExpanded()).isTrue();
+ }
+
+ @Test
+ public void testShowOrHideAppBubble_expandIfCollapsed() {
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+ mBubbleController.updateBubble(mBubbleEntry);
+ mBubbleController.collapseStack();
+ assertThat(mBubbleController.isStackExpanded()).isFalse();
+
+ // Calling this while collapsed will expand the app bubble
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+ assertThat(mBubbleController.isStackExpanded()).isTrue();
+ assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
+ }
+
+ @Test
+ public void testShowOrHideAppBubble_collapseIfSelected() {
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+ assertThat(mBubbleController.isStackExpanded()).isTrue();
+
+ // Calling this while the app bubble is expanded should collapse the stack
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+ assertThat(mBubbleController.isStackExpanded()).isFalse();
+ assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
+ }
+
+ @Test
+ public void testShowOrHideAppBubble_selectIfNotSelected() {
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+ mBubbleController.updateBubble(mBubbleEntry);
+ mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey());
+ assertThat(mBubbleController.isStackExpanded()).isTrue();
+
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE);
+ assertThat(mBubbleController.isStackExpanded()).isTrue();
+ assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
+ }
+
/** Creates a bubble using the userId and package. */
private Bubble createBubble(int userId, String pkg) {
final UserHandle userHandle = new UserHandle(userId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index e5316bc8..ceee0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -24,6 +24,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
@@ -48,7 +49,8 @@
Handler mainHandler,
NotifPipelineFlags flags,
KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
- UiEventLogger uiEventLogger) {
+ UiEventLogger uiEventLogger,
+ UserTracker userTracker) {
super(contentResolver,
powerManager,
dreamManager,
@@ -61,7 +63,8 @@
mainHandler,
flags,
keyguardNotificationVisibilityProvider,
- uiEventLogger);
+ uiEventLogger,
+ userTracker);
mUseHeadsUp = true;
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 7ae47b4..45489d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.notetask.NoteTaskInitializer;
+import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -63,6 +64,7 @@
@RunWith(AndroidJUnit4.class)
public class WMShellTest extends SysuiTestCase {
WMShell mWMShell;
+ FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
@Mock ShellInterface mShellInterface;
@Mock CommandQueue mCommandQueue;
@@ -100,6 +102,7 @@
mProtoTracer,
mWakefulnessLifecycle,
mUserTracker,
+ mDisplayTracker,
mNoteTaskInitializer,
mSysUiMainExecutor
);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
index 3767fbe..3428553 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java
@@ -40,24 +40,49 @@
public class MemoryTrackingTestCase extends SysuiTestCase {
private static File sFilesDir = null;
private static String sLatestTestClassName = null;
+ private static int sHeapCount = 0;
+ private static File sLatestBaselineHeapFile = null;
- @Before public void grabFilesDir() {
+ // Ideally, we would do this in @BeforeClass just once, but we need mContext to get the files
+ // dir, and that does not exist until @Before on each test method.
+ @Before
+ public void grabFilesDir() throws IOException {
+ // This should happen only once per suite
if (sFilesDir == null) {
sFilesDir = mContext.getFilesDir();
}
- sLatestTestClassName = getClass().getName();
+
+ // This will happen before the first test method in each class
+ if (sLatestTestClassName == null) {
+ sLatestTestClassName = getClass().getName();
+ sLatestBaselineHeapFile = dump("baseline" + (++sHeapCount), "before-test");
+ }
}
@AfterClass
public static void dumpHeap() throws IOException {
+ File afterTestHeap = dump(sLatestTestClassName, "after-test");
+ if (sLatestBaselineHeapFile != null && afterTestHeap != null) {
+ Log.w("MEMORY", "To compare heap to baseline (use go/ahat):");
+ Log.w("MEMORY", " adb pull " + sLatestBaselineHeapFile);
+ Log.w("MEMORY", " adb pull " + afterTestHeap);
+ Log.w("MEMORY",
+ " java -jar ahat.jar --baseline " + sLatestBaselineHeapFile.getName() + " "
+ + afterTestHeap.getName());
+ }
+ sLatestTestClassName = null;
+ }
+
+ private static File dump(String basename, String heapKind) throws IOException {
if (sFilesDir == null) {
Log.e("MEMORY", "Somehow no test cases??");
- return;
+ return null;
}
mockitoTearDown();
- Log.w("MEMORY", "about to dump heap");
- File path = new File(sFilesDir, sLatestTestClassName + ".ahprof");
+ Log.w("MEMORY", "about to dump " + heapKind + " heap");
+ File path = new File(sFilesDir, basename + ".ahprof");
Debug.dumpHprofData(path.getPath());
- Log.w("MEMORY", "did it! Location: " + path);
+ Log.w("MEMORY", "Success! Location: " + path);
+ return path;
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index bf2235a..1bab997 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -132,8 +132,10 @@
@After
public void SysuiTeardown() {
- InstrumentationRegistry.registerInstance(mRealInstrumentation,
- InstrumentationRegistry.getArguments());
+ if (mRealInstrumentation != null) {
+ InstrumentationRegistry.registerInstance(mRealInstrumentation,
+ InstrumentationRegistry.getArguments());
+ }
if (TestableLooper.get(this) != null) {
TestableLooper.get(this).processAllMessages();
// Must remove static reference to this test object to prevent leak (b/261039202)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
index 990db77..f723a9e5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
@@ -23,14 +23,20 @@
isUnlocked: Boolean = true,
isShowingAlternateAuthOnUnlock: Boolean = false,
interactionJankMonitor: InteractionJankMonitor = mock(InteractionJankMonitor::class.java),
+ isPredictiveBackQsDialogAnim: Boolean = false,
): DialogLaunchAnimator {
return DialogLaunchAnimator(
- FakeCallback(
- isUnlocked = isUnlocked,
- isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
- ),
- interactionJankMonitor,
- fakeLaunchAnimator(),
+ callback =
+ FakeCallback(
+ isUnlocked = isUnlocked,
+ isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
+ ),
+ interactionJankMonitor = interactionJankMonitor,
+ featureFlags =
+ object : AnimationFeatureFlags {
+ override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
+ },
+ launchAnimator = fakeLaunchAnimator(),
isForTesting = true,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
index 8176dd0..1bdee36 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt
@@ -19,9 +19,10 @@
import android.graphics.Rect
class FakeOverlapDetector : OverlapDetector {
- var shouldReturn: Boolean = false
+ var shouldReturn: Map<Int, Boolean> = mapOf()
override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean {
- return shouldReturn
+ return shouldReturn[touchData.pointerId]
+ ?: error("Unexpected PointerId not declared in TestCase currentPointers")
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 6c82cef..b94f816e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -38,12 +38,6 @@
}
}
- fun set(flag: DeviceConfigBooleanFlag, value: Boolean) {
- if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
- notifyFlagChanged(flag)
- }
- }
-
fun set(flag: ResourceBooleanFlag, value: Boolean) {
if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
notifyFlagChanged(flag)
@@ -73,7 +67,7 @@
listeners.forEach { listener ->
listener.onFlagChanged(
object : FlagListenable.FlagEvent {
- override val flagId = flag.id
+ override val flagName = flag.name
override fun requestNoRestart() {}
}
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
new file mode 100644
index 0000000..044679d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeBiometricSettingsRepository : BiometricSettingsRepository {
+
+ private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false)
+ override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow()
+
+ private val _isStrongBiometricAllowed = MutableStateFlow(false)
+ override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow()
+
+ private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false)
+ override val isFingerprintEnabledByDevicePolicy =
+ _isFingerprintEnabledByDevicePolicy.asStateFlow()
+
+ fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
+ _isFingerprintEnrolled.value = isFingerprintEnrolled
+ }
+
+ fun setStrongBiometricAllowed(isStrongBiometricAllowed: Boolean) {
+ _isStrongBiometricAllowed.value = isStrongBiometricAllowed
+ }
+
+ fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) {
+ _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
new file mode 100644
index 0000000..5641832
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepository {
+ private val _isLockedOut = MutableStateFlow<Boolean>(false)
+ override val isLockedOut: StateFlow<Boolean> = _isLockedOut.asStateFlow()
+
+ fun setLockedOut(lockedOut: Boolean) {
+ _isLockedOut.value = lockedOut
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt
new file mode 100644
index 0000000..d0383e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardBouncerRepository.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Fake implementation of [KeyguardRepository] */
+class FakeKeyguardBouncerRepository : KeyguardBouncerRepository {
+ private val _primaryBouncerVisible = MutableStateFlow(false)
+ override val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+ private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
+ override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+ private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+ override val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+ private val _primaryBouncerHide = MutableStateFlow(false)
+ override val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+ private val _primaryBouncerStartingToHide = MutableStateFlow(false)
+ override val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+ private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
+ override val primaryBouncerStartingDisappearAnimation =
+ _primaryBouncerDisappearAnimation.asStateFlow()
+ private val _primaryBouncerScrimmed = MutableStateFlow(false)
+ override val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+ private val _panelExpansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
+ override val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
+ private val _keyguardPosition = MutableStateFlow(0f)
+ override val keyguardPosition = _keyguardPosition.asStateFlow()
+ private val _onScreenTurnedOff = MutableStateFlow(false)
+ override val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+ private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
+ override val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+ private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
+ override val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+ private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
+ override val showMessage = _showMessage.asStateFlow()
+ private val _resourceUpdateRequests = MutableStateFlow(false)
+ override val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+ override val bouncerPromptReason = 0
+ override val bouncerErrorMessage: CharSequence? = null
+ private val _isAlternateBouncerVisible = MutableStateFlow(false)
+ override val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow()
+ override var lastAlternateBouncerVisibleTime: Long = 0L
+ private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false)
+ override val isAlternateBouncerUIAvailable = _isAlternateBouncerUIAvailable.asStateFlow()
+
+ override fun setPrimaryScrimmed(isScrimmed: Boolean) {
+ _primaryBouncerScrimmed.value = isScrimmed
+ }
+
+ override fun setPrimaryVisible(isVisible: Boolean) {
+ _primaryBouncerVisible.value = isVisible
+ }
+
+ override fun setAlternateVisible(isVisible: Boolean) {
+ _isAlternateBouncerVisible.value = isVisible
+ }
+
+ override fun setAlternateBouncerUIAvailable(isAvailable: Boolean) {
+ _isAlternateBouncerUIAvailable.value = isAvailable
+ }
+
+ override fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+ _primaryBouncerShow.value = keyguardBouncerModel
+ }
+
+ override fun setPrimaryShowingSoon(showingSoon: Boolean) {
+ _primaryBouncerShowingSoon.value = showingSoon
+ }
+
+ override fun setPrimaryHide(hide: Boolean) {
+ _primaryBouncerHide.value = hide
+ }
+
+ override fun setPrimaryStartingToHide(startingToHide: Boolean) {
+ _primaryBouncerStartingToHide.value = startingToHide
+ }
+
+ override fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+ _primaryBouncerDisappearAnimation.value = runnable
+ }
+
+ override fun setPanelExpansion(panelExpansion: Float) {
+ _panelExpansionAmount.value = panelExpansion
+ }
+
+ override fun setKeyguardPosition(keyguardPosition: Float) {
+ _keyguardPosition.value = keyguardPosition
+ }
+
+ override fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+ _resourceUpdateRequests.value = willUpdateResources
+ }
+
+ override fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+ _showMessage.value = bouncerShowMessageModel
+ }
+
+ override fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+ _keyguardAuthenticated.value = keyguardAuthenticated
+ }
+
+ override fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+ _isBackButtonEnabled.value = isBackButtonEnabled
+ }
+
+ override fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
+ _onScreenTurnedOff.value = onScreenTurnedOff
+ }
+}
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 39d2eca..065fe89 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
@@ -29,6 +29,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
/** Fake implementation of [KeyguardRepository] */
class FakeKeyguardRepository : KeyguardRepository {
@@ -52,6 +53,9 @@
private val _isDozing = MutableStateFlow(false)
override val isDozing: Flow<Boolean> = _isDozing
+ private val _isAodAvailable = MutableStateFlow(false)
+ override val isAodAvailable: Flow<Boolean> = _isAodAvailable
+
private val _isDreaming = MutableStateFlow(false)
override val isDreaming: Flow<Boolean> = _isDreaming
@@ -98,6 +102,13 @@
private val _biometricUnlockSource = MutableStateFlow<BiometricUnlockSource?>(null)
override val biometricUnlockSource: Flow<BiometricUnlockSource?> = _biometricUnlockSource
+ private val _isQuickSettingsVisible = MutableStateFlow(false)
+ override val isQuickSettingsVisible: Flow<Boolean> = _isQuickSettingsVisible.asStateFlow()
+
+ override fun setQuickSettingsVisible(isVisible: Boolean) {
+ _isQuickSettingsVisible.value = isVisible
+ }
+
override fun isKeyguardShowing(): Boolean {
return _isKeyguardShowing.value
}
@@ -126,6 +137,10 @@
_isDozing.value = isDozing
}
+ fun setAodAvailable(isAodAvailable: Boolean) {
+ _isAodAvailable.value = isAodAvailable
+ }
+
fun setDreamingWithOverlay(isDreaming: Boolean) {
_isDreamingWithOverlay.value = isDreaming
}
@@ -162,6 +177,10 @@
_dozeTransitionModel.value = model
}
+ fun setStatusBarState(state: StatusBarState) {
+ _statusBarState.value = state
+ }
+
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 6c44244..eac1bd1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -22,13 +22,15 @@
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import java.util.UUID
+import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
/** Fake implementation of [KeyguardTransitionRepository] */
class FakeKeyguardTransitionRepository : KeyguardTransitionRepository {
- private val _transitions = MutableSharedFlow<TransitionStep>()
+ private val _transitions =
+ MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val transitions: SharedFlow<TransitionStep> = _transitions
suspend fun sendTransitionStep(step: TransitionStep) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
new file mode 100644
index 0000000..6ae7c34
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeDisplayTracker.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.settings
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.view.Display
+import java.util.concurrent.Executor
+
+class FakeDisplayTracker internal constructor(val context: Context) : DisplayTracker {
+ val displayManager: DisplayManager = context.getSystemService(DisplayManager::class.java)!!
+ override var defaultDisplayId: Int = Display.DEFAULT_DISPLAY
+ override var allDisplays: Array<Display> = displayManager.displays
+
+ private val displayCallbacks: MutableList<DisplayTracker.Callback> = ArrayList()
+ private val brightnessCallbacks: MutableList<DisplayTracker.Callback> = ArrayList()
+ override fun addDisplayChangeCallback(callback: DisplayTracker.Callback, executor: Executor) {
+ displayCallbacks.add(callback)
+ }
+ override fun addBrightnessChangeCallback(
+ callback: DisplayTracker.Callback,
+ executor: Executor
+ ) {
+ brightnessCallbacks.add(callback)
+ }
+
+ override fun removeCallback(callback: DisplayTracker.Callback) {
+ displayCallbacks.remove(callback)
+ brightnessCallbacks.remove(callback)
+ }
+
+ fun setDefaultDisplay(displayId: Int) {
+ defaultDisplayId = displayId
+ }
+
+ fun setDisplays(displays: Array<Display>) {
+ allDisplays = displays
+ }
+
+ fun triggerOnDisplayAdded(displayId: Int) {
+ notifyCallbacks({ onDisplayAdded(displayId) }, displayCallbacks)
+ }
+
+ fun triggerOnDisplayRemoved(displayId: Int) {
+ notifyCallbacks({ onDisplayRemoved(displayId) }, displayCallbacks)
+ }
+
+ fun triggerOnDisplayChanged(displayId: Int) {
+ notifyCallbacks({ onDisplayChanged(displayId) }, displayCallbacks)
+ }
+
+ fun triggerOnDisplayBrightnessChanged(displayId: Int) {
+ notifyCallbacks({ onDisplayChanged(displayId) }, brightnessCallbacks)
+ }
+
+ private inline fun notifyCallbacks(
+ crossinline action: DisplayTracker.Callback.() -> Unit,
+ list: List<DisplayTracker.Callback>
+ ) {
+ list.forEach { it.action() }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index 0dd1fc7..251014f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -67,7 +67,10 @@
_userHandle = UserHandle.of(_userId)
val copy = callbacks.toList()
- copy.forEach { it.onUserChanged(_userId, userContext) }
+ copy.forEach {
+ it.onUserChanging(_userId, userContext)
+ it.onUserChanged(_userId, userContext)
+ }
}
fun onProfileChanged() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
index 045e6f1..7bcad45 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
+
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -57,6 +59,7 @@
private ShortcutInfo mShortcutInfo = null;
private int mRankingAdjustment = 0;
private boolean mIsBubble = false;
+ private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
public RankingBuilder() {
}
@@ -86,6 +89,7 @@
mShortcutInfo = ranking.getConversationShortcutInfo();
mRankingAdjustment = ranking.getRankingAdjustment();
mIsBubble = ranking.isBubble();
+ mProposedImportance = ranking.getProposedImportance();
}
public Ranking build() {
@@ -114,7 +118,8 @@
mIsConversation,
mShortcutInfo,
mRankingAdjustment,
- mIsBubble);
+ mIsBubble,
+ mProposedImportance);
return ranking;
}
@@ -214,6 +219,11 @@
return this;
}
+ public RankingBuilder setProposedImportance(@Importance int importance) {
+ mProposedImportance = importance;
+ return this;
+ }
+
public RankingBuilder setUserSentiment(int userSentiment) {
mUserSentiment = userSentiment;
return this;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index ea5a302..1a8e244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -39,6 +39,10 @@
private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+ private val _userSwitchingInProgress = MutableStateFlow(false)
+ override val userSwitchingInProgress: Flow<Boolean>
+ get() = _userSwitchingInProgress
+
override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
private var _isGuestUserAutoCreated: Boolean = false
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
index 4a881a7..fd1b8e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
@@ -41,7 +41,7 @@
}
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
- return data.getOrDefault(key, defValues) as? MutableSet<String>?
+ return (data.getOrDefault(key, defValues) as? Set<String>?)?.toMutableSet()
}
override fun getInt(key: String, defValue: Int): Int {
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
index e660e1f2..4b97316 100644
--- 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
@@ -23,6 +23,8 @@
import android.os.UserHandle;
import android.util.Pair;
+import com.android.systemui.settings.UserTracker;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -57,6 +59,11 @@
}
@Override
+ public UserTracker getUserTracker() {
+ return null;
+ }
+
+ @Override
public void registerContentObserverForUser(Uri uri, boolean notifyDescendents,
ContentObserver settingsObserver, int userHandle) {
List<ContentObserver> observers;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
index 33ef9cf..1baac84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
@@ -46,11 +46,6 @@
}
@Override
- public boolean hasEmergencyCryptKeeperText() {
- return false;
- }
-
- @Override
public boolean isRadioOn() {
return false;
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
index 2d6d29a..926c6c5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
@@ -98,6 +98,10 @@
}
@Override
+ public void removeAllIconsForExternalSlot(String slot) {
+ }
+
+ @Override
public void setIconAccessibilityLiveRegion(String slot, int mode) {
}
diff --git a/packages/SystemUI/tools/lint/baseline.xml b/packages/SystemUI/tools/lint/baseline.xml
index 9a2e320..301c9b8 100644
--- a/packages/SystemUI/tools/lint/baseline.xml
+++ b/packages/SystemUI/tools/lint/baseline.xml
@@ -337,17 +337,6 @@
<issue
id="Deprecated"
message="`android:singleLine` is deprecated: Use `maxLines="1"` instead"
- errorLine1=" android:singleLine="true""
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="res/layout/emergency_cryptkeeper_text.xml"
- line="25"
- column="9"/>
- </issue>
-
- <issue
- id="Deprecated"
- message="`android:singleLine` is deprecated: Use `maxLines="1"` instead"
errorLine1=" android:singleLine="true""
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -88997,17 +88986,6 @@
errorLine1=" android:paddingStart="6dp""
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
- file="res/layout/emergency_cryptkeeper_text.xml"
- line="24"
- column="9"/>
- </issue>
-
- <issue
- id="RtlSymmetry"
- message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry"
- errorLine1=" android:paddingStart="6dp""
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
file="res/layout/heads_up_status_bar_layout.xml"
line="43"
column="9"/>
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
index 5a868a4..cfb959e 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt
@@ -22,8 +22,8 @@
import android.os.Handler
import android.view.IWindowManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
-import com.android.systemui.unfold.dagger.UnfoldBackground
import com.android.systemui.unfold.dagger.UnfoldMain
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
@@ -58,7 +58,7 @@
@BindsInstance sensorManager: SensorManager,
@BindsInstance @UnfoldMain handler: Handler,
@BindsInstance @UnfoldMain executor: Executor,
- @BindsInstance @UnfoldBackground backgroundExecutor: Executor,
+ @BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
@BindsInstance windowManager: IWindowManager,
@BindsInstance contentResolver: ContentResolver = context.contentResolver
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
index 3fa5469..31616fa 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt
@@ -16,9 +16,7 @@
package com.android.systemui.unfold
-import android.hardware.SensorManager
import com.android.systemui.unfold.config.UnfoldTransitionConfig
-import com.android.systemui.unfold.dagger.UnfoldBackground
import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider
import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.updates.DeviceFoldStateProvider
@@ -34,55 +32,18 @@
import dagger.Module
import dagger.Provides
import java.util.Optional
-import java.util.concurrent.Executor
+import javax.inject.Provider
import javax.inject.Singleton
-@Module
+@Module(includes = [UnfoldSharedInternalModule::class])
class UnfoldSharedModule {
@Provides
@Singleton
- fun unfoldTransitionProgressProvider(
- config: UnfoldTransitionConfig,
- scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
- tracingListener: ATraceLoggerTransitionProgressListener,
- foldStateProvider: FoldStateProvider
- ): Optional<UnfoldTransitionProgressProvider> =
- if (!config.isEnabled) {
- Optional.empty()
- } else {
- val baseProgressProvider =
- if (config.isHingeAngleEnabled) {
- PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
- } else {
- FixedTimingTransitionProgressProvider(foldStateProvider)
- }
- Optional.of(
- scaleAwareProviderFactory.wrap(baseProgressProvider).apply {
- // Always present callback that logs animation beginning and end.
- addCallback(tracingListener)
- }
- )
- }
-
- @Provides
- @Singleton
fun provideFoldStateProvider(
deviceFoldStateProvider: DeviceFoldStateProvider
): FoldStateProvider = deviceFoldStateProvider
@Provides
- fun hingeAngleProvider(
- config: UnfoldTransitionConfig,
- sensorManager: SensorManager,
- @UnfoldBackground executor: Executor
- ): HingeAngleProvider =
- if (config.isHingeAngleEnabled) {
- HingeSensorAngleProvider(sensorManager, executor)
- } else {
- EmptyHingeAngleProvider
- }
-
- @Provides
@Singleton
fun unfoldKeyguardVisibilityProvider(
impl: UnfoldKeyguardVisibilityManagerImpl
@@ -94,3 +55,51 @@
impl: UnfoldKeyguardVisibilityManagerImpl
): UnfoldKeyguardVisibilityManager = impl
}
+
+/**
+ * Needed as methods inside must be public, but their parameters can be internal (and, a public
+ * method can't have internal parameters). Making the module internal and included in a public one
+ * fixes the issue.
+ */
+@Module
+internal class UnfoldSharedInternalModule {
+ @Provides
+ @Singleton
+ fun unfoldTransitionProgressProvider(
+ config: UnfoldTransitionConfig,
+ scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
+ tracingListener: ATraceLoggerTransitionProgressListener,
+ physicsBasedUnfoldTransitionProgressProvider:
+ Provider<PhysicsBasedUnfoldTransitionProgressProvider>,
+ fixedTimingTransitionProgressProvider: Provider<FixedTimingTransitionProgressProvider>,
+ ): Optional<UnfoldTransitionProgressProvider> {
+ if (!config.isEnabled) {
+ return Optional.empty()
+ }
+ val baseProgressProvider =
+ if (config.isHingeAngleEnabled) {
+ physicsBasedUnfoldTransitionProgressProvider.get()
+ } else {
+ fixedTimingTransitionProgressProvider.get()
+ }
+
+ return Optional.of(
+ scaleAwareProviderFactory.wrap(baseProgressProvider).apply {
+ // Always present callback that logs animation beginning and end.
+ addCallback(tracingListener)
+ }
+ )
+ }
+
+ @Provides
+ fun hingeAngleProvider(
+ config: UnfoldTransitionConfig,
+ hingeAngleSensorProvider: Provider<HingeSensorAngleProvider>
+ ): HingeAngleProvider {
+ return if (config.isHingeAngleEnabled) {
+ hingeAngleSensorProvider.get()
+ } else {
+ EmptyHingeAngleProvider
+ }
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
index a1ed178..aa93c629 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt
@@ -37,29 +37,29 @@
* This should **never** be called from sysui, as the object is already provided in that process.
*/
fun createUnfoldSharedComponent(
- context: Context,
- config: UnfoldTransitionConfig,
- screenStatusProvider: ScreenStatusProvider,
- foldProvider: FoldProvider,
- activityTypeProvider: CurrentActivityTypeProvider,
- sensorManager: SensorManager,
- mainHandler: Handler,
- mainExecutor: Executor,
- backgroundExecutor: Executor,
- tracingTagPrefix: String,
- windowManager: IWindowManager,
+ context: Context,
+ config: UnfoldTransitionConfig,
+ screenStatusProvider: ScreenStatusProvider,
+ foldProvider: FoldProvider,
+ activityTypeProvider: CurrentActivityTypeProvider,
+ sensorManager: SensorManager,
+ mainHandler: Handler,
+ mainExecutor: Executor,
+ singleThreadBgExecutor: Executor,
+ tracingTagPrefix: String,
+ windowManager: IWindowManager,
): UnfoldSharedComponent =
- DaggerUnfoldSharedComponent.factory()
- .create(
- context,
- config,
- screenStatusProvider,
- foldProvider,
- activityTypeProvider,
- sensorManager,
- mainHandler,
- mainExecutor,
- backgroundExecutor,
- tracingTagPrefix,
- windowManager,
- )
+ DaggerUnfoldSharedComponent.factory()
+ .create(
+ context,
+ config,
+ screenStatusProvider,
+ foldProvider,
+ activityTypeProvider,
+ sensorManager,
+ mainHandler,
+ mainExecutor,
+ singleThreadBgExecutor,
+ tracingTagPrefix,
+ windowManager,
+ )
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt
similarity index 89%
rename from packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt
rename to packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt
index 6074795..dcac531 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldBackground.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldSingleThreadBg.kt
@@ -18,8 +18,7 @@
/**
* Alternative to [UiBackground] qualifier annotation in unfold module.
+ *
* It is needed as we can't depend on SystemUI code in this module.
*/
-@Qualifier
-@Retention(AnnotationRetention.RUNTIME)
-annotation class UnfoldBackground
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnfoldSingleThreadBg
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
index fa59cb4..4622464 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/FixedTimingTransitionProgressProvider.kt
@@ -24,11 +24,13 @@
import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
+import javax.inject.Inject
/** Emits animation progress with fixed timing after unfolding */
-internal class FixedTimingTransitionProgressProvider(
- private val foldStateProvider: FoldStateProvider
-) : UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
+internal class FixedTimingTransitionProgressProvider
+@Inject
+constructor(private val foldStateProvider: FoldStateProvider) :
+ UnfoldTransitionProgressProvider, FoldStateProvider.FoldUpdatesListener {
private val animatorListener = AnimatorListener()
private val animator =
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index 074b1e1..6ffbe5a 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -33,9 +33,10 @@
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.unfold.updates.name
+import javax.inject.Inject
/** Maps fold updates to unfold transition progress using DynamicAnimation. */
-class PhysicsBasedUnfoldTransitionProgressProvider(
+class PhysicsBasedUnfoldTransitionProgressProvider @Inject constructor(
private val foldStateProvider: FoldStateProvider
) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 5b45897..97c9ba9 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -79,6 +79,7 @@
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
rotationChangeProvider.addCallback(rotationListener)
+ activityTypeProvider.init()
}
override fun stop() {
@@ -87,6 +88,7 @@
hingeAngleProvider.removeCallback(hingeAngleListener)
hingeAngleProvider.stop()
rotationChangeProvider.removeCallback(rotationListener)
+ activityTypeProvider.uninit()
}
override fun addCallback(listener: FoldUpdatesListener) {
@@ -115,19 +117,17 @@
}
val isClosing = angle < lastHingeAngle
- val closingThreshold = getClosingThreshold()
- val closingThresholdMet = closingThreshold == null || angle < closingThreshold
val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
val screenAvailableEventSent = isUnfoldHandled
if (isClosing // hinge angle should be decreasing since last update
- && closingThresholdMet // hinge angle is below certain threshold
&& !closingEventDispatched // we haven't sent closing event already
&& !isFullyOpened // do not send closing event if we are in fully opened hinge
// angle range as closing threshold could overlap this range
&& screenAvailableEventSent // do not send closing event if we are still in
// the process of turning on the inner display
+ && isClosingThresholdMet(angle) // hinge angle is below certain threshold.
) {
notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
}
@@ -146,6 +146,11 @@
outputListeners.forEach { it.onHingeAngleUpdate(angle) }
}
+ private fun isClosingThresholdMet(currentAngle: Float) : Boolean {
+ val closingThreshold = getClosingThreshold()
+ return closingThreshold == null || currentAngle < closingThreshold
+ }
+
/**
* Fold animation should be started only after the threshold returned here.
*
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
index 577137c..89fb12e 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
@@ -20,35 +20,43 @@
import android.hardware.SensorManager
import android.os.Trace
import androidx.core.util.Consumer
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import java.util.concurrent.Executor
+import javax.inject.Inject
-internal class HingeSensorAngleProvider(
+internal class HingeSensorAngleProvider
+@Inject
+constructor(
private val sensorManager: SensorManager,
- private val executor: Executor
-) :
- HingeAngleProvider {
+ @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor
+) : HingeAngleProvider {
private val sensorListener = HingeAngleSensorListener()
private val listeners: MutableList<Consumer<Float>> = arrayListOf()
var started = false
- override fun start() = executor.execute {
- if (started) return@execute
- Trace.beginSection("HingeSensorAngleProvider#start")
- val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
- sensorManager.registerListener(
- sensorListener,
- sensor,
- SensorManager.SENSOR_DELAY_FASTEST
- )
- Trace.endSection()
- started = true
+ override fun start() {
+ singleThreadBgExecutor.execute {
+ if (started) return@execute
+ Trace.beginSection("HingeSensorAngleProvider#start")
+ val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
+ sensorManager.registerListener(
+ sensorListener,
+ sensor,
+ SensorManager.SENSOR_DELAY_FASTEST
+ )
+ Trace.endSection()
+
+ started = true
+ }
}
- override fun stop() = executor.execute {
- if (!started) return@execute
- sensorManager.unregisterListener(sensorListener)
- started = false
+ override fun stop() {
+ singleThreadBgExecutor.execute {
+ if (!started) return@execute
+ sensorManager.unregisterListener(sensorListener)
+ started = false
+ }
}
override fun removeCallback(listener: Consumer<Float>) {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
index d0e6cdc..34e7c38 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/CurrentActivityTypeProvider.kt
@@ -16,6 +16,11 @@
interface CurrentActivityTypeProvider {
val isHomeActivity: Boolean?
+
+ /** Starts listening for task updates. */
+ fun init() {}
+ /** Stop listening for task updates. */
+ fun uninit() {}
}
class EmptyCurrentActivityTypeProvider(override val isHomeActivity: Boolean? = null) :
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index 3fa0ab6..e6abc4c 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -391,7 +391,7 @@
private boolean takeScreenshot() {
ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
- screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ screenshotHelper.takeScreenshot(
WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
new Handler(Looper.getMainLooper()), null);
return true;
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 308f360..9d91b97 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -817,7 +817,8 @@
if (host != null) {
host.callbacks = null;
pruneHostLocked(host);
- mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
+ mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(),
+ false);
}
}
}
@@ -888,12 +889,8 @@
Host host = lookupHostLocked(id);
if (host != null) {
- try {
- mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
- } catch (NullPointerException e) {
- Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e);
- throw e;
- }
+ mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(),
+ false);
}
}
}
@@ -4345,14 +4342,15 @@
PendingHostUpdate.appWidgetRemoved(appWidgetId));
}
- public SparseArray<String> getWidgetUids() {
+ public SparseArray<String> getWidgetUidsIfBound() {
final SparseArray<String> uids = new SparseArray<>();
for (int i = widgets.size() - 1; i >= 0; i--) {
final Widget widget = widgets.get(i);
if (widget.provider == null) {
if (DEBUG) {
- Slog.e(TAG, "Widget with no provider " + widget.toString());
+ Slog.d(TAG, "Widget with no provider " + widget.toString());
}
+ continue;
}
final ProviderId providerId = widget.provider.id;
uids.put(providerId.uid, providerId.componentName.getPackageName());
diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
index 677871f..8c2c964 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -357,6 +357,7 @@
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title);
params.windowAnimations = R.style.AutofillSaveAnimation;
+ params.setTrustedOverlay();
show();
}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index fdfcfa3..a95f899 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3769,6 +3769,13 @@
final boolean includeSharedProfile =
(flags & StorageManager.FLAG_INCLUDE_SHARED_PROFILE) != 0;
+ // When the caller is the app actually hosting external storage, we
+ // should never attempt to augment the actual storage volume state,
+ // otherwise we risk confusing it with race conditions as users go
+ // through various unlocked states
+ final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
+ mMediaStoreAuthorityAppId);
+
// Only Apps with MANAGE_EXTERNAL_STORAGE should call the API with includeSharedProfile
if (includeSharedProfile) {
try {
@@ -3781,8 +3788,13 @@
// Checking first entry in packagesFromUid is enough as using "sharedUserId"
// mechanism is rare and discouraged. Also, Apps that share same UID share the same
// permissions.
- if (!mStorageManagerInternal.hasExternalStorageAccess(callingUid,
- packagesFromUid[0])) {
+ // Allowing Media Provider is an exception, Media Provider process should be allowed
+ // to query users across profiles, even without MANAGE_EXTERNAL_STORAGE access.
+ // Note that ordinarily Media provider process has the above permission, but if they
+ // are revoked, Storage Volume(s) should still be returned.
+ if (!callerIsMediaStore
+ && !mStorageManagerInternal.hasExternalStorageAccess(callingUid,
+ packagesFromUid[0])) {
throw new SecurityException("Only File Manager Apps permitted");
}
} catch (RemoteException re) {
@@ -3795,13 +3807,6 @@
// point
final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM);
- // When the caller is the app actually hosting external storage, we
- // should never attempt to augment the actual storage volume state,
- // otherwise we risk confusing it with race conditions as users go
- // through various unlocked states
- final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
- mMediaStoreAuthorityAppId);
-
final boolean userIsDemo;
final boolean userKeyUnlocked;
final boolean storagePermission;
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 89447b4..f846741 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -3091,7 +3091,7 @@
}
}
- Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+ Intent intent = result.getParcelable(AccountManager.KEY_INTENT, Intent.class);
if (intent != null && notifyOnAuthFailure && !customTokens) {
/*
* Make sure that the supplied intent is owned by the authenticator
@@ -3516,8 +3516,7 @@
Bundle.setDefusable(result, true);
mNumResults++;
Intent intent = null;
- if (result != null
- && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
+ if (result != null) {
if (!checkKeyIntent(
Binder.getCallingUid(),
result)) {
@@ -4886,8 +4885,10 @@
EventLog.writeEvent(0x534e4554, "250588548", authUid, "");
return false;
}
-
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
+ if (intent == null) {
+ return true;
+ }
// Explicitly set an empty ClipData to ensure that we don't offer to
// promote any Uris contained inside for granting purposes
if (intent.getClipData() == null) {
@@ -4937,8 +4938,12 @@
Bundle simulateBundle = p.readBundle();
p.recycle();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
- return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT,
- Intent.class)));
+ Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT,
+ Intent.class);
+ if (intent == null) {
+ return (simulateIntent == null);
+ }
+ return intent.filterEquals(simulateIntent);
}
private boolean isExportedSystemActivity(ActivityInfo activityInfo) {
@@ -5087,8 +5092,7 @@
}
}
}
- if (result != null
- && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
+ if (result != null) {
if (!checkKeyIntent(
Binder.getCallingUid(),
result)) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 9669c06..c36e070 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3420,6 +3420,11 @@
throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
+ className + " is not an isolatedProcess");
}
+ if (!mAm.getPackageManagerInternal().isSameApp(callingPackage, callingUid,
+ userId)) {
+ throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
+ + "calling package not owned by calling UID ");
+ }
// Run the service under the calling package's application.
ApplicationInfo aInfo = AppGlobals.getPackageManager().getApplicationInfo(
callingPackage, ActivityManagerService.STOCK_PM_FLAGS, userId);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a59f0a3..d48723a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -8197,15 +8197,13 @@
t.traceEnd();
}
+ boolean isBootingSystemUser = currentUserId == UserHandle.USER_SYSTEM;
+
// Some systems - like automotive - will explicitly unlock system user then switch
- // to a secondary user. Hence, we don't want to send duplicate broadcasts for
- // the system user here.
+ // to a secondary user.
// TODO(b/242195409): this workaround shouldn't be necessary once we move
// the headless-user start logic to UserManager-land.
- final boolean isBootingSystemUser = (currentUserId == UserHandle.USER_SYSTEM)
- && !UserManager.isHeadlessSystemUserMode();
-
- if (isBootingSystemUser) {
+ if (isBootingSystemUser && !UserManager.isHeadlessSystemUserMode()) {
t.traceBegin("startHomeOnAllDisplays");
mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
t.traceEnd();
@@ -8217,6 +8215,10 @@
if (isBootingSystemUser) {
+ // Need to send the broadcasts for the system user here because
+ // UserController#startUserInternal will not send them for the system user starting,
+ // It checks if the user state already exists, which is always the case for the
+ // system user.
t.traceBegin("sendUserStartBroadcast");
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 8624ee0..bda60ff 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -379,16 +379,11 @@
resolvedType = key.requestResolvedType;
}
- // Apply any launch flags from the ActivityOptions. This is used only by SystemUI
- // to ensure that we can launch the pending intent with a consistent launch mode even
- // if the provided PendingIntent is immutable (ie. to force an activity to launch into
- // a new task, or to launch multiple instances if supported by the app)
+ // Apply any launch flags from the ActivityOptions. This is to ensure that the caller
+ // can specify a consistent launch mode even if the PendingIntent is immutable
final ActivityOptions opts = ActivityOptions.fromBundle(options);
if (opts != null) {
- // TODO(b/254490217): Move this check into SafeActivityOptions
- if (controller.mAtmInternal.isCallerRecents(Binder.getCallingUid())) {
- finalIntent.addFlags(opts.getPendingIntentLaunchFlags());
- }
+ finalIntent.addFlags(opts.getPendingIntentLaunchFlags());
}
// Extract options before clearing calling identity
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index 3584f16..ac78228 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -51,6 +51,7 @@
import android.app.GameModeInfo;
import android.app.GameState;
import android.app.IGameManagerService;
+import android.app.IUidObserver;
import android.app.compat.PackageOverride;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -118,6 +119,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Service to manage game related features.
@@ -171,40 +173,19 @@
private final ArrayMap<String, GamePackageConfiguration> mOverrideConfigs = new ArrayMap<>();
@Nullable
private final GameServiceController mGameServiceController;
+ private final Object mUidObserverLock = new Object();
+ @VisibleForTesting
+ @Nullable
+ final UidObserver mUidObserver;
+ @GuardedBy("mUidObserverLock")
+ private final Set<Integer> mForegroundGameUids = new HashSet<>();
public GameManagerService(Context context) {
this(context, createServiceThread().getLooper());
}
GameManagerService(Context context, Looper looper) {
- mContext = context;
- mHandler = new SettingsHandler(looper);
- mPackageManager = mContext.getPackageManager();
- mUserManager = mContext.getSystemService(UserManager.class);
- mPlatformCompat = IPlatformCompat.Stub.asInterface(
- ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
- mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
- mSystemDir = new File(Environment.getDataDirectory(), "system");
- mSystemDir.mkdirs();
- FileUtils.setPermissions(mSystemDir.toString(),
- FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH,
- -1, -1);
- mGameModeInterventionListFile = new AtomicFile(new File(mSystemDir,
- GAME_MODE_INTERVENTION_LIST_FILE_NAME));
- FileUtils.setPermissions(mGameModeInterventionListFile.getBaseFile().getAbsolutePath(),
- FileUtils.S_IRUSR | FileUtils.S_IWUSR
- | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
- -1, -1);
- if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_GAME_SERVICE)) {
- mGameServiceController = new GameServiceController(
- context, BackgroundThread.getExecutor(),
- new GameServiceProviderSelectorImpl(
- context.getResources(),
- context.getPackageManager()),
- new GameServiceProviderInstanceFactoryImpl(context));
- } else {
- mGameServiceController = null;
- }
+ this(context, looper, Environment.getDataDirectory());
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@@ -237,6 +218,14 @@
} else {
mGameServiceController = null;
}
+ mUidObserver = new UidObserver();
+ try {
+ ActivityManager.getService().registerUidObserver(mUidObserver,
+ ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
+ ActivityManager.PROCESS_STATE_UNKNOWN, null);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Could not register UidObserver");
+ }
}
@Override
@@ -1874,4 +1863,66 @@
* load dynamic library for frame rate overriding JNI calls
*/
private static native void nativeSetOverrideFrameRate(int uid, float frameRate);
+
+ final class UidObserver extends IUidObserver.Stub {
+ @Override
+ public void onUidIdle(int uid, boolean disabled) {}
+
+ @Override
+ public void onUidGone(int uid, boolean disabled) {
+ synchronized (mUidObserverLock) {
+ disableGameMode(uid);
+ }
+ }
+
+ @Override
+ public void onUidActive(int uid) {}
+
+ @Override
+ public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
+ synchronized (mUidObserverLock) {
+ if (ActivityManager.isProcStateBackground(procState)) {
+ disableGameMode(uid);
+ return;
+ }
+
+ final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+ if (packages == null || packages.length == 0) {
+ return;
+ }
+
+ final int userId = mContext.getUserId();
+ if (!Arrays.stream(packages).anyMatch(p -> isPackageGame(p, userId))) {
+ return;
+ }
+
+ if (mForegroundGameUids.isEmpty()) {
+ Slog.v(TAG, "Game power mode ON (process state was changed to foreground)");
+ mPowerManagerInternal.setPowerMode(Mode.GAME, true);
+ }
+ mForegroundGameUids.add(uid);
+ }
+ }
+
+ private void disableGameMode(int uid) {
+ synchronized (mUidObserverLock) {
+ if (!mForegroundGameUids.contains(uid)) {
+ return;
+ }
+ mForegroundGameUids.remove(uid);
+ if (!mForegroundGameUids.isEmpty()) {
+ return;
+ }
+ Slog.v(TAG,
+ "Game power mode OFF (process remomved or state changed to background)");
+ mPowerManagerInternal.setPowerMode(Mode.GAME, false);
+ }
+ }
+
+ @Override
+ public void onUidCachedChanged(int uid, boolean cached) {}
+
+ @Override
+ public void onUidProcAdjChanged(int uid) {}
+ }
}
diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
index 4fd739ca..4b0ae1b 100644
--- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
+++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
@@ -16,6 +16,9 @@
package com.android.server.app;
+import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
+import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE;
+
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,7 +36,6 @@
import android.graphics.Insets;
import android.graphics.Rect;
import android.net.Uri;
-import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.games.CreateGameSessionRequest;
@@ -50,7 +52,6 @@
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost.SurfacePackage;
-import android.view.WindowManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -59,6 +60,7 @@
import com.android.internal.infra.ServiceConnector.ServiceLifecycleCallbacks;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;
import com.android.server.wm.WindowManagerInternal.TaskSystemBarsListener;
@@ -861,8 +863,6 @@
Slog.w(TAG, "Could not get bitmap for id: " + taskId);
callback.complete(GameScreenshotResult.createInternalErrorResult());
} else {
- final Bundle bundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(
- bitmap);
final RunningTaskInfo runningTaskInfo =
mGameTaskInfoProvider.getRunningTaskInfo(taskId);
if (runningTaskInfo == null) {
@@ -877,11 +877,17 @@
callback.complete(GameScreenshotResult.createSuccessResult());
}
};
- mScreenshotHelper.provideScreenshot(bundle, crop, Insets.NONE, taskId,
- mUserHandle.getIdentifier(), gameSessionRecord.getComponentName(),
- WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
- BackgroundThread.getHandler(),
- completionConsumer);
+ ScreenshotRequest request = new ScreenshotRequest.Builder(
+ TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
+ .setTopComponent(gameSessionRecord.getComponentName())
+ .setTaskId(taskId)
+ .setUserId(mUserHandle.getIdentifier())
+ .setBitmap(bitmap)
+ .setBoundsOnScreen(crop)
+ .setInsets(Insets.NONE)
+ .build();
+ mScreenshotHelper.takeScreenshot(
+ request, BackgroundThread.getHandler(), completionConsumer);
}
});
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 730c410..5051755 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -56,6 +56,7 @@
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
@@ -82,6 +83,7 @@
private final @NonNull AudioService mAudioService;
private final @NonNull Context mContext;
+ private final @NonNull AudioSystemAdapter mAudioSystem;
/** ID for Communication strategy retrieved form audio policy manager */
private int mCommunicationStrategyId = -1;
@@ -156,12 +158,14 @@
public static final long USE_SET_COMMUNICATION_DEVICE = 243827847L;
//-------------------------------------------------------------------
- /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
+ /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
+ @NonNull AudioSystemAdapter audioSystem) {
mContext = context;
mAudioService = service;
mBtHelper = new BtHelper(this);
mDeviceInventory = new AudioDeviceInventory(this);
mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
+ mAudioSystem = audioSystem;
init();
}
@@ -170,12 +174,14 @@
* in system_server */
AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
@NonNull AudioDeviceInventory mockDeviceInventory,
- @NonNull SystemServerAdapter mockSystemServer) {
+ @NonNull SystemServerAdapter mockSystemServer,
+ @NonNull AudioSystemAdapter audioSystem) {
mContext = context;
mAudioService = service;
mBtHelper = new BtHelper(this);
mDeviceInventory = mockDeviceInventory;
mSystemServer = mockSystemServer;
+ mAudioSystem = audioSystem;
init();
}
@@ -429,6 +435,48 @@
return device;
}
+ private static final int[] VALID_COMMUNICATION_DEVICE_TYPES = {
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ AudioDeviceInfo.TYPE_USB_HEADSET,
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
+ AudioDeviceInfo.TYPE_HEARING_AID,
+ AudioDeviceInfo.TYPE_BLE_HEADSET,
+ AudioDeviceInfo.TYPE_USB_DEVICE,
+ AudioDeviceInfo.TYPE_BLE_SPEAKER,
+ AudioDeviceInfo.TYPE_LINE_ANALOG,
+ AudioDeviceInfo.TYPE_HDMI,
+ AudioDeviceInfo.TYPE_AUX_LINE
+ };
+
+ /*package */ static boolean isValidCommunicationDevice(AudioDeviceInfo device) {
+ for (int type : VALID_COMMUNICATION_DEVICE_TYPES) {
+ if (device.getType() == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /* package */ static List<AudioDeviceInfo> getAvailableCommunicationDevices() {
+ ArrayList<AudioDeviceInfo> commDevices = new ArrayList<>();
+ AudioDeviceInfo[] allDevices =
+ AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : allDevices) {
+ if (isValidCommunicationDevice(device)) {
+ commDevices.add(device);
+ }
+ }
+ return commDevices;
+ }
+
+ private @Nullable AudioDeviceInfo getCommunicationDeviceOfType(int type) {
+ return getAvailableCommunicationDevices().stream().filter(d -> d.getType() == type)
+ .findFirst().orElse(null);
+ }
+
/**
* Returns the device currently requested for communication use case.
* @return AudioDeviceInfo the requested device for communication.
@@ -436,7 +484,29 @@
/* package */ AudioDeviceInfo getCommunicationDevice() {
synchronized (mDeviceStateLock) {
updateActiveCommunicationDevice();
- return mActiveCommunicationDevice;
+ AudioDeviceInfo device = mActiveCommunicationDevice;
+ // make sure we return a valid communication device (i.e. a device that is allowed by
+ // setCommunicationDevice()) for consistency.
+ if (device != null) {
+ // a digital dock is used instead of the speaker in speakerphone mode and should
+ // be reflected as such
+ if (device.getType() == AudioDeviceInfo.TYPE_DOCK) {
+ device = getCommunicationDeviceOfType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
+ }
+ }
+ // Try to default to earpiece when current communication device is not valid. This can
+ // happen for instance if no call is active. If no earpiece device is available take the
+ // first valid communication device
+ if (device == null || !AudioDeviceBroker.isValidCommunicationDevice(device)) {
+ device = getCommunicationDeviceOfType(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ if (device == null) {
+ List<AudioDeviceInfo> commDevices = getAvailableCommunicationDevices();
+ if (!commDevices.isEmpty()) {
+ device = commDevices.get(0);
+ }
+ }
+ }
+ return device;
}
}
@@ -450,7 +520,7 @@
AudioAttributes attr =
AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType(
AudioSystem.STREAM_VOICE_CALL);
- List<AudioDeviceAttributes> devices = AudioSystem.getDevicesForAttributes(
+ List<AudioDeviceAttributes> devices = mAudioSystem.getDevicesForAttributes(
attr, false /* forVolume */);
if (devices.isEmpty()) {
if (mAudioService.isPlatformVoice()) {
@@ -913,8 +983,8 @@
@GuardedBy("mDeviceStateLock")
private void dispatchCommunicationDevice() {
- int portId = (mActiveCommunicationDevice == null) ? 0
- : mActiveCommunicationDevice.getId();
+ AudioDeviceInfo device = getCommunicationDevice();
+ int portId = device != null ? device.getId() : 0;
if (portId == mCurCommunicationPortId) {
return;
}
@@ -931,6 +1001,7 @@
mCommDevDispatchers.finishBroadcast();
}
+
//---------------------------------------------------------------------
// Communication with (to) AudioService
//TODO check whether the AudioService methods are candidates to move here
@@ -1225,7 +1296,7 @@
Log.v(TAG, "onSetForceUse(useCase<" + useCase + ">, config<" + config + ">, fromA2dp<"
+ fromA2dp + ">, eventSource<" + eventSource + ">)");
}
- AudioSystem.setForceUse(useCase, config);
+ mAudioSystem.setForceUse(useCase, config);
}
private void onSendBecomingNoisyIntent() {
@@ -1863,9 +1934,9 @@
if (preferredCommunicationDevice == null
|| preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
- AudioSystem.setParameters("BT_SCO=off");
+ mAudioSystem.setParameters("BT_SCO=off");
} else {
- AudioSystem.setParameters("BT_SCO=on");
+ mAudioSystem.setParameters("BT_SCO=on");
}
if (preferredCommunicationDevice == null) {
AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 5275b7c..5907354 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -186,6 +186,7 @@
import com.android.server.SystemService;
import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent;
import com.android.server.audio.AudioServiceEvents.PhoneStateEvent;
+import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent;
import com.android.server.audio.AudioServiceEvents.VolumeEvent;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
@@ -1208,7 +1209,7 @@
mUseFixedVolume = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_useFixedVolume);
- mDeviceBroker = new AudioDeviceBroker(mContext, this);
+ mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem);
mRecordMonitor = new RecordingActivityMonitor(mContext);
mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true);
@@ -1255,6 +1256,20 @@
0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
}
+ private void initVolumeStreamStates() {
+ int numStreamTypes = AudioSystem.getNumStreamTypes();
+ synchronized (VolumeStreamState.class) {
+ for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
+ VolumeStreamState streamState = mStreamStates[streamType];
+ int groupId = getVolumeGroupForStreamType(streamType);
+ if (groupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP
+ && sVolumeGroupStates.indexOfKey(groupId) >= 0) {
+ streamState.setVolumeGroupState(sVolumeGroupStates.get(groupId));
+ }
+ }
+ }
+ }
+
/**
* Separating notification volume from ring is NOT of aliasing the corresponding streams
* @param properties
@@ -1284,6 +1299,8 @@
// mSafeUsbMediaVolumeIndex must be initialized after createStreamStates() because it
// relies on audio policy having correct ranges for volume indexes.
mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
+ // Link VGS on VSS
+ initVolumeStreamStates();
// Call setRingerModeInt() to apply correct mute
// state on streams affected by ringer mode.
@@ -1640,7 +1657,7 @@
synchronized (mSettingsLock) {
final int forDock = mDockAudioMediaEnabled ?
- AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE;
+ AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE;
mDeviceBroker.setForceUse_Async(AudioSystem.FOR_DOCK, forDock, "onAudioServerDied");
sendEncodedSurroundMode(mContentResolver, "onAudioServerDied");
sendEnabledSurroundFormats(mContentResolver, true);
@@ -2180,19 +2197,19 @@
AudioSystem.STREAM_ASSISTANT : AudioSystem.STREAM_MUSIC;
if (mIsSingleVolume) {
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_TELEVISION.clone();
dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
} else if (mUseVolumeGroupAliases) {
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_NONE.clone();
dtmfStreamAlias = AudioSystem.STREAM_DTMF;
} else {
switch (mPlatformType) {
case AudioSystem.PLATFORM_VOICE:
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_VOICE.clone();
dtmfStreamAlias = AudioSystem.STREAM_RING;
break;
default:
- mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT;
+ mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT.clone();
dtmfStreamAlias = AudioSystem.STREAM_MUSIC;
}
if (!mNotifAliasRing) {
@@ -2261,9 +2278,10 @@
SENDMSG_QUEUE,
AudioSystem.FOR_DOCK,
mDockAudioMediaEnabled ?
- AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE,
+ AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE,
new String("readDockAudioSettings"),
0);
+
}
@@ -3341,15 +3359,7 @@
} else {
state = direction == AudioManager.ADJUST_MUTE;
}
- for (int stream = 0; stream < mStreamStates.length; stream++) {
- if (streamTypeAlias == mStreamVolumeAlias[stream]) {
- if (!(readCameraSoundForced()
- && (mStreamStates[stream].getStreamType()
- == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
- mStreamStates[stream].mute(state);
- }
- }
- }
+ muteAliasStreams(streamTypeAlias, state);
} else if ((direction == AudioManager.ADJUST_RAISE) &&
!checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
@@ -3364,7 +3374,7 @@
// Unmute the stream if it was previously muted
if (direction == AudioManager.ADJUST_RAISE) {
// unmute immediately for volume up
- streamState.mute(false);
+ muteAliasStreams(streamTypeAlias, false);
} else if (direction == AudioManager.ADJUST_LOWER) {
if (mIsSingleVolume) {
sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE,
@@ -3490,6 +3500,42 @@
sendVolumeUpdate(streamType, oldIndex, newIndex, flags, device);
}
+ /**
+ * Loops on aliasted stream, update the mute cache attribute of each
+ * {@see AudioService#VolumeStreamState}, and then apply the change.
+ * It prevents to unnecessary {@see AudioSystem#setStreamVolume} done for each stream
+ * and aliases before mute change changed and after.
+ */
+ private void muteAliasStreams(int streamAlias, boolean state) {
+ synchronized (VolumeStreamState.class) {
+ List<Integer> streamsToMute = new ArrayList<>();
+ for (int stream = 0; stream < mStreamStates.length; stream++) {
+ if (streamAlias == mStreamVolumeAlias[stream]) {
+ if (!(readCameraSoundForced()
+ && (mStreamStates[stream].getStreamType()
+ == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
+ boolean changed = mStreamStates[stream].mute(state, /* apply= */ false);
+ if (changed) {
+ streamsToMute.add(stream);
+ }
+ }
+ }
+ }
+ streamsToMute.forEach(streamToMute -> {
+ mStreamStates[streamToMute].doMute();
+ broadcastMuteSetting(streamToMute, state);
+ });
+ }
+ }
+
+ private void broadcastMuteSetting(int streamType, boolean isMuted) {
+ // Stream mute changed, fire the intent.
+ Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+ intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
+ intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, isMuted);
+ sendBroadcastToAll(intent);
+ }
+
// Called after a delay when volume down is pressed while muted
private void onUnmuteStream(int stream, int flags) {
boolean wasMuted;
@@ -3594,8 +3640,19 @@
return false;
}
+ /**
+ * Update stream volume, ringer mode and mute status after a volume index change
+ * @param streamType
+ * @param index
+ * @param flags
+ * @param device the device for which the volume is changed
+ * @param caller
+ * @param hasModifyAudioSettings
+ * @param canChangeMute true if the origin of this event is one where the mute state should be
+ * updated following the change in volume index
+ */
private void onSetStreamVolume(int streamType, int index, int flags, int device,
- String caller, boolean hasModifyAudioSettings) {
+ String caller, boolean hasModifyAudioSettings, boolean canChangeMute) {
final int stream = mStreamVolumeAlias[streamType];
setStreamVolumeInt(stream, index, device, false, caller, hasModifyAudioSettings);
// setting volume on ui sounds stream type also controls silent mode
@@ -3604,10 +3661,11 @@
setRingerMode(getNewRingerMode(stream, index, flags),
TAG + ".onSetStreamVolume", false /*external*/);
}
- // setting non-zero volume for a muted stream unmutes the stream and vice versa,
+ // setting non-zero volume for a muted stream unmutes the stream and vice versa
// except for BT SCO stream where only explicit mute is allowed to comply to BT requirements
- if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) {
- mStreamStates[stream].mute(index == 0);
+ if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) && canChangeMute) {
+ // As adjustStreamVolume with muteAdjust flags mute/unmutes stream and aliased streams.
+ muteAliasStreams(stream, index == 0);
}
}
@@ -3663,29 +3721,27 @@
}
- /** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */
- public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags,
+ /** @see AudioManager#setVolumeGroupVolumeIndex(int, int, int) */
+ public void setVolumeGroupVolumeIndex(int groupId, int index, int flags,
String callingPackage, String attributionTag) {
enforceModifyAudioRoutingPermission();
- Objects.requireNonNull(attr, "attr must not be null");
- final int volumeGroup = getVolumeGroupIdForAttributes(attr);
- if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
- Log.e(TAG, ": no volume group found for attributes " + attr.toString());
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ Log.e(TAG, ": no volume group found for id " + groupId);
return;
}
- final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
- sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(),
- index/*val1*/, flags/*val2*/, callingPackage));
+ sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, vgs.name(),
+ index, flags, callingPackage + ", user " + getCurrentUserId()));
vgs.setVolumeIndex(index, flags);
// For legacy reason, propagate to all streams associated to this volume group
- for (final int groupedStream : vgs.getLegacyStreamTypes()) {
+ for (int groupedStream : vgs.getLegacyStreamTypes()) {
try {
ensureValidStreamType(groupedStream);
} catch (IllegalArgumentException e) {
- Log.d(TAG, "volume group " + volumeGroup + " has internal streams (" + groupedStream
+ Log.d(TAG, "volume group " + groupId + " has internal streams (" + groupedStream
+ "), do not change associated stream volume");
continue;
}
@@ -3697,7 +3753,7 @@
@Nullable
private AudioVolumeGroup getAudioVolumeGroupById(int volumeGroupId) {
- for (final AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) {
+ for (AudioVolumeGroup avg : AudioVolumeGroup.getAudioVolumeGroups()) {
if (avg.getId() == volumeGroupId) {
return avg;
}
@@ -3707,30 +3763,42 @@
return null;
}
- /** @see AudioManager#getVolumeIndexForAttributes(attr) */
- public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+ /** @see AudioManager#getVolumeGroupVolumeIndex(int) */
+ public int getVolumeGroupVolumeIndex(int groupId) {
enforceModifyAudioRoutingPermission();
- Objects.requireNonNull(attr, "attr must not be null");
- final int volumeGroup = getVolumeGroupIdForAttributes(attr);
- if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
- throw new IllegalArgumentException("No volume group for attributes " + attr);
+ synchronized (VolumeStreamState.class) {
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ throw new IllegalArgumentException("No volume group for id " + groupId);
+ }
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+ // Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero
+ // min but it mutable on permission condition.
+ return vgs.isMuted() ? 0 : vgs.getVolumeIndex();
}
- final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
- return vgs.getVolumeIndex();
}
- /** @see AudioManager#getMaxVolumeIndexForAttributes(attr) */
- public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+ /** @see AudioManager#getVolumeGroupMaxVolumeIndex(int) */
+ public int getVolumeGroupMaxVolumeIndex(int groupId) {
enforceModifyAudioRoutingPermission();
- Objects.requireNonNull(attr, "attr must not be null");
- return AudioSystem.getMaxVolumeIndexForAttributes(attr);
+ synchronized (VolumeStreamState.class) {
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ throw new IllegalArgumentException("No volume group for id " + groupId);
+ }
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+ return vgs.getMaxIndex();
+ }
}
- /** @see AudioManager#getMinVolumeIndexForAttributes(attr) */
- public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+ /** @see AudioManager#getVolumeGroupMinVolumeIndex(int) */
+ public int getVolumeGroupMinVolumeIndex(int groupId) {
enforceModifyAudioRoutingPermission();
- Objects.requireNonNull(attr, "attr must not be null");
- return AudioSystem.getMinVolumeIndexForAttributes(attr);
+ synchronized (VolumeStreamState.class) {
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ throw new IllegalArgumentException("No volume group for id " + groupId);
+ }
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+ return vgs.getMinIndex();
+ }
}
/** @see AudioDeviceVolumeManager#setDeviceVolume(VolumeInfo, AudioDeviceAttributes)
@@ -3744,19 +3812,30 @@
Objects.requireNonNull(ada);
Objects.requireNonNull(callingPackage);
- AudioService.sVolumeLogger.loglogi("setDeviceVolume" + " from:" + callingPackage + " "
- + vi + " " + ada, TAG);
-
if (!vi.hasStreamType()) {
Log.e(TAG, "Unsupported non-stream type based VolumeInfo", new Exception());
return;
}
+
int index = vi.getVolumeIndex();
if (index == VolumeInfo.INDEX_NOT_SET && !vi.hasMuteCommand()) {
throw new IllegalArgumentException(
"changing device volume requires a volume index or mute command");
}
+ // force a cache clear to force reevaluating stream type to audio device selection
+ // that can interfere with the sending of the VOLUME_CHANGED_ACTION intent
+ // TODO change cache management to not rely only on invalidation, but on "do not trust"
+ // moments when routing is in flux.
+ mAudioSystem.clearRoutingCache();
+
+ // log the current device that will be used when evaluating the sending of the
+ // VOLUME_CHANGED_ACTION intent to see if the current device is the one being modified
+ final int currDev = getDeviceForStream(vi.getStreamType());
+
+ AudioService.sVolumeLogger.log(new DeviceVolumeEvent(vi.getStreamType(), index, ada,
+ currDev, callingPackage));
+
// TODO handle unmuting of current audio device
// if a stream is not muted but the VolumeInfo is for muting, set the volume index
// for the device to min volume
@@ -3796,6 +3875,70 @@
callingPackage, /*attributionTag*/ null);
}
+ /** @see AudioManager#adjustVolumeGroupVolume(int, int, int) */
+ public void adjustVolumeGroupVolume(int groupId, int direction, int flags,
+ String callingPackage) {
+ ensureValidDirection(direction);
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ Log.e(TAG, ": no volume group found for id " + groupId);
+ return;
+ }
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+ // For compatibility reason, use stream API if group linked to a valid stream
+ boolean fallbackOnStream = false;
+ for (int stream : vgs.getLegacyStreamTypes()) {
+ try {
+ ensureValidStreamType(stream);
+ } catch (IllegalArgumentException e) {
+ Log.d(TAG, "volume group " + groupId + " has internal streams (" + stream
+ + "), do not change associated stream volume");
+ continue;
+ }
+ // Note: Group and Stream does not share same convention, 0 is mute for stream,
+ // min index is acting as mute for Groups
+ if (vgs.isVssMuteBijective(stream)) {
+ adjustStreamVolume(stream, direction, flags, callingPackage);
+ if (isMuteAdjust(direction)) {
+ // will be propagated to all aliased streams
+ return;
+ }
+ fallbackOnStream = true;
+ }
+ }
+ if (fallbackOnStream) {
+ // Handled by at least one stream, will be propagated to group, bailing out.
+ return;
+ }
+ sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_GROUP_VOL, vgs.name(),
+ direction, flags, callingPackage));
+ vgs.adjustVolume(direction, flags);
+ }
+
+ /** @see AudioManager#getLastAudibleVolumeGroupVolume(int) */
+ public int getLastAudibleVolumeGroupVolume(int groupId) {
+ enforceQueryStatePermission();
+ synchronized (VolumeStreamState.class) {
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ Log.e(TAG, ": no volume group found for id " + groupId);
+ return 0;
+ }
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+ return vgs.getVolumeIndex();
+ }
+ }
+
+ /** @see AudioManager#isVolumeGroupMuted(int) */
+ public boolean isVolumeGroupMuted(int groupId) {
+ synchronized (VolumeStreamState.class) {
+ if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
+ Log.e(TAG, ": no volume group found for id " + groupId);
+ return false;
+ }
+ VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
+ return vgs.isMuted();
+ }
+ }
+
/** @see AudioManager#setStreamVolume(int, int, int)
* Part of service interface, check permissions here */
public void setStreamVolumeWithAttribution(int streamType, int index, int flags,
@@ -3840,11 +3983,11 @@
return;
}
- final AudioEventLogger.Event event = (device == null)
- ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
- index/*val1*/, flags/*val2*/, callingPackage)
- : new DeviceVolumeEvent(streamType, index, device, callingPackage);
- sVolumeLogger.log(event);
+ if (device == null) {
+ // call was already logged in setDeviceVolume()
+ sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
+ index/*val1*/, flags/*val2*/, callingPackage));
+ }
setStreamVolume(streamType, index, flags, device,
callingPackage, callingPackage, attributionTag,
Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission());
@@ -4235,7 +4378,10 @@
mPendingVolumeCommand = new StreamVolumeCommand(
streamType, index, flags, device);
} else {
- onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings);
+ onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
+ // ada is non-null when called from setDeviceVolume,
+ // which shouldn't update the mute state
+ ada == null /*canChangeMute*/);
index = mStreamStates[streamType].getIndex(device);
}
}
@@ -4245,32 +4391,11 @@
maybeSendSystemAudioStatusCommand(false);
}
}
- sendVolumeUpdate(streamType, oldIndex, index, flags, device);
- }
-
-
-
- private int getVolumeGroupIdForAttributes(@NonNull AudioAttributes attributes) {
- Objects.requireNonNull(attributes, "attributes must not be null");
- int volumeGroupId = getVolumeGroupIdForAttributesInt(attributes);
- if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
- return volumeGroupId;
+ if (ada == null) {
+ // only non-null when coming here from setDeviceVolume
+ // TODO change test to check early if device is current device or not
+ sendVolumeUpdate(streamType, oldIndex, index, flags, device);
}
- // The default volume group is the one hosted by default product strategy, i.e.
- // supporting Default Attributes
- return getVolumeGroupIdForAttributesInt(AudioProductStrategy.getDefaultAttributes());
- }
-
- private int getVolumeGroupIdForAttributesInt(@NonNull AudioAttributes attributes) {
- Objects.requireNonNull(attributes, "attributes must not be null");
- for (final AudioProductStrategy productStrategy :
- AudioProductStrategy.getAudioProductStrategies()) {
- int volumeGroupId = productStrategy.getVolumeGroupIdForAudioAttributes(attributes);
- if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
- return volumeGroupId;
- }
- }
- return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
}
private void dispatchAbsoluteVolumeChanged(int streamType, AbsoluteVolumeDeviceInfo deviceInfo,
@@ -4305,7 +4430,6 @@
}
}
-
// No ringer or zen muted stream volumes can be changed unless it'll exit dnd
private boolean volumeAdjustmentAllowedByDnd(int streamTypeAlias, int flags) {
switch (mNm.getZenMode()) {
@@ -4997,7 +5121,7 @@
}
private void setRingerMode(int ringerMode, String caller, boolean external) {
- if (mUseFixedVolume || mIsSingleVolume) {
+ if (mUseFixedVolume || mIsSingleVolume || mUseVolumeGroupAliases) {
return;
}
if (caller == null || caller.length() == 0) {
@@ -5822,7 +5946,7 @@
}
}
- readVolumeGroupsSettings();
+ readVolumeGroupsSettings(userSwitch);
if (DEBUG_VOL) {
Log.d(TAG, "Restoring device volume behavior");
@@ -5830,46 +5954,16 @@
restoreDeviceVolumeBehavior();
}
- private static final int[] VALID_COMMUNICATION_DEVICE_TYPES = {
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
- AudioDeviceInfo.TYPE_WIRED_HEADSET,
- AudioDeviceInfo.TYPE_USB_HEADSET,
- AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
- AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
- AudioDeviceInfo.TYPE_HEARING_AID,
- AudioDeviceInfo.TYPE_BLE_HEADSET,
- AudioDeviceInfo.TYPE_USB_DEVICE,
- AudioDeviceInfo.TYPE_BLE_SPEAKER,
- AudioDeviceInfo.TYPE_LINE_ANALOG,
- AudioDeviceInfo.TYPE_HDMI,
- AudioDeviceInfo.TYPE_AUX_LINE
- };
-
- private boolean isValidCommunicationDevice(AudioDeviceInfo device) {
- for (int type : VALID_COMMUNICATION_DEVICE_TYPES) {
- if (device.getType() == type) {
- return true;
- }
- }
- return false;
- }
-
/** @see AudioManager#getAvailableCommunicationDevices(int) */
public int[] getAvailableCommunicationDeviceIds() {
- ArrayList<Integer> deviceIds = new ArrayList<>();
- AudioDeviceInfo[] devices = AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : devices) {
- if (isValidCommunicationDevice(device)) {
- deviceIds.add(device.getId());
- }
- }
- return deviceIds.stream().mapToInt(Integer::intValue).toArray();
+ List<AudioDeviceInfo> commDevices = AudioDeviceBroker.getAvailableCommunicationDevices();
+ return commDevices.stream().mapToInt(AudioDeviceInfo::getId).toArray();
}
- /**
- * @see AudioManager#setCommunicationDevice(int)
- * @see AudioManager#clearCommunicationDevice()
- */
+
+ /**
+ * @see AudioManager#setCommunicationDevice(int)
+ * @see AudioManager#clearCommunicationDevice()
+ */
public boolean setCommunicationDevice(IBinder cb, int portId) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
@@ -5878,9 +5972,10 @@
if (portId != 0) {
device = AudioManager.getDeviceForPortId(portId, AudioManager.GET_DEVICES_OUTPUTS);
if (device == null) {
- throw new IllegalArgumentException("invalid portID " + portId);
+ Log.w(TAG, "setCommunicationDevice: invalid portID " + portId);
+ return false;
}
- if (!isValidCommunicationDevice(device)) {
+ if (!AudioDeviceBroker.isValidCommunicationDevice(device)) {
throw new IllegalArgumentException("invalid device type " + device.getType());
}
}
@@ -5923,13 +6018,15 @@
/** @see AudioManager#getCommunicationDevice() */
public int getCommunicationDevice() {
+ int deviceId = 0;
final long ident = Binder.clearCallingIdentity();
- AudioDeviceInfo device = mDeviceBroker.getCommunicationDevice();
- Binder.restoreCallingIdentity(ident);
- if (device == null) {
- return 0;
+ try {
+ AudioDeviceInfo device = mDeviceBroker.getCommunicationDevice();
+ deviceId = device != null ? device.getId() : 0;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
- return device.getId();
+ return deviceId;
}
/** @see AudioManager#addOnCommunicationDeviceChangedListener(
@@ -7301,6 +7398,7 @@
try {
// if no valid attributes, this volume group is not controllable, throw exception
ensureValidAttributes(avg);
+ sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
} catch (IllegalArgumentException e) {
// Volume Groups without attributes are not controllable through set/get volume
// using attributes. Do not append them.
@@ -7309,11 +7407,10 @@
}
continue;
}
- sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
}
for (int i = 0; i < sVolumeGroupStates.size(); i++) {
final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
- vgs.applyAllVolumes();
+ vgs.applyAllVolumes(/* userSwitch= */ false);
}
}
@@ -7326,14 +7423,22 @@
}
}
- private void readVolumeGroupsSettings() {
- if (DEBUG_VOL) {
- Log.v(TAG, "readVolumeGroupsSettings");
- }
- for (int i = 0; i < sVolumeGroupStates.size(); i++) {
- final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
- vgs.readSettings();
- vgs.applyAllVolumes();
+ private void readVolumeGroupsSettings(boolean userSwitch) {
+ synchronized (mSettingsLock) {
+ synchronized (VolumeStreamState.class) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "readVolumeGroupsSettings userSwitch=" + userSwitch);
+ }
+ for (int i = 0; i < sVolumeGroupStates.size(); i++) {
+ VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
+ // as for STREAM_MUSIC, preserve volume from one user to the next.
+ if (!(userSwitch && vgs.isMusic())) {
+ vgs.clearIndexCache();
+ vgs.readSettings();
+ }
+ vgs.applyAllVolumes(userSwitch);
+ }
+ }
}
}
@@ -7344,7 +7449,7 @@
}
for (int i = 0; i < sVolumeGroupStates.size(); i++) {
final VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
- vgs.applyAllVolumes();
+ vgs.applyAllVolumes(false/*userSwitch*/);
}
}
@@ -7357,17 +7462,34 @@
}
}
+ private static boolean isCallStream(int stream) {
+ return stream == AudioSystem.STREAM_VOICE_CALL
+ || stream == AudioSystem.STREAM_BLUETOOTH_SCO;
+ }
+
+ private static int getVolumeGroupForStreamType(int stream) {
+ AudioAttributes attributes =
+ AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType(stream);
+ if (attributes.equals(new AudioAttributes.Builder().build())) {
+ return AudioVolumeGroup.DEFAULT_VOLUME_GROUP;
+ }
+ return AudioProductStrategy.getVolumeGroupIdForAudioAttributes(
+ attributes, /* fallbackOnDefault= */ false);
+ }
+
// NOTE: Locking order for synchronized objects related to volume management:
// 1 mSettingsLock
- // 2 VolumeGroupState.class
+ // 2 VolumeStreamState.class
private class VolumeGroupState {
private final AudioVolumeGroup mAudioVolumeGroup;
private final SparseIntArray mIndexMap = new SparseIntArray(8);
private int mIndexMin;
private int mIndexMax;
- private int mLegacyStreamType = AudioSystem.STREAM_DEFAULT;
+ private boolean mHasValidStreamType = false;
private int mPublicStreamType = AudioSystem.STREAM_MUSIC;
private AudioAttributes mAudioAttributes = AudioProductStrategy.getDefaultAttributes();
+ private boolean mIsMuted = false;
+ private final String mSettingName;
// No API in AudioSystem to get a device from strategy or from attributes.
// Need a valid public stream type to use current API getDeviceForStream
@@ -7381,20 +7503,22 @@
Log.v(TAG, "VolumeGroupState for " + avg.toString());
}
// mAudioAttributes is the default at this point
- for (final AudioAttributes aa : avg.getAudioAttributes()) {
+ for (AudioAttributes aa : avg.getAudioAttributes()) {
if (!aa.equals(mAudioAttributes)) {
mAudioAttributes = aa;
break;
}
}
- final int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes();
+ int[] streamTypes = mAudioVolumeGroup.getLegacyStreamTypes();
+ String streamSettingName = "";
if (streamTypes.length != 0) {
// Uses already initialized MIN / MAX if a stream type is attached to group
- mLegacyStreamType = streamTypes[0];
- for (final int streamType : streamTypes) {
+ for (int streamType : streamTypes) {
if (streamType != AudioSystem.STREAM_DEFAULT
&& streamType < AudioSystem.getNumStreamTypes()) {
mPublicStreamType = streamType;
+ mHasValidStreamType = true;
+ streamSettingName = System.VOLUME_SETTINGS_INT[mPublicStreamType];
break;
}
}
@@ -7404,10 +7528,10 @@
mIndexMin = AudioSystem.getMinVolumeIndexForAttributes(mAudioAttributes);
mIndexMax = AudioSystem.getMaxVolumeIndexForAttributes(mAudioAttributes);
} else {
- Log.e(TAG, "volume group: " + mAudioVolumeGroup.name()
+ throw new IllegalArgumentException("volume group: " + mAudioVolumeGroup.name()
+ " has neither valid attributes nor valid stream types assigned");
- return;
}
+ mSettingName = !streamSettingName.isEmpty() ? streamSettingName : ("volume_" + name());
// Load volume indexes from data base
readSettings();
}
@@ -7420,40 +7544,149 @@
return mAudioVolumeGroup.name();
}
+ /**
+ * Volume group with non null minimum index are considered as non mutable, thus
+ * bijectivity is broken with potential associated stream type.
+ * VOICE_CALL stream has minVolumeIndex > 0 but can be muted directly by an
+ * app that has MODIFY_PHONE_STATE permission.
+ */
+ private boolean isVssMuteBijective(int stream) {
+ return isStreamAffectedByMute(stream)
+ && (getMinIndex() == (mStreamStates[stream].mIndexMin + 5) / 10)
+ && (getMinIndex() == 0 || isCallStream(stream));
+ }
+
+ private boolean isMutable() {
+ return mIndexMin == 0 || (mHasValidStreamType && isVssMuteBijective(mPublicStreamType));
+ }
+ /**
+ * Mute/unmute the volume group
+ * @param muted the new mute state
+ */
+ @GuardedBy("AudioService.VolumeStreamState.class")
+ public boolean mute(boolean muted) {
+ if (!isMutable()) {
+ // Non mutable volume group
+ if (DEBUG_VOL) {
+ Log.d(TAG, "invalid mute on unmutable volume group " + name());
+ }
+ return false;
+ }
+ boolean changed = (mIsMuted != muted);
+ // As for VSS, mute shall apply minIndex to all devices found in IndexMap and default.
+ if (changed) {
+ mIsMuted = muted;
+ applyAllVolumes(false /*userSwitch*/);
+ }
+ return changed;
+ }
+
+ public boolean isMuted() {
+ return mIsMuted;
+ }
+
+ public void adjustVolume(int direction, int flags) {
+ synchronized (VolumeStreamState.class) {
+ int device = getDeviceForVolume();
+ int previousIndex = getIndex(device);
+ if (isMuteAdjust(direction) && !isMutable()) {
+ // Non mutable volume group
+ if (DEBUG_VOL) {
+ Log.d(TAG, "invalid mute on unmutable volume group " + name());
+ }
+ return;
+ }
+ switch (direction) {
+ case AudioManager.ADJUST_TOGGLE_MUTE: {
+ // Note: If muted by volume 0, unmute will restore volume 0.
+ mute(!mIsMuted);
+ break;
+ }
+ case AudioManager.ADJUST_UNMUTE:
+ // Note: If muted by volume 0, unmute will restore volume 0.
+ mute(false);
+ break;
+ case AudioManager.ADJUST_MUTE:
+ // May be already muted by setvolume 0, prevent from setting same value
+ if (previousIndex != 0) {
+ // bypass persist
+ mute(true);
+ }
+ mIsMuted = true;
+ break;
+ case AudioManager.ADJUST_RAISE:
+ // As for stream, RAISE during mute will increment the index
+ setVolumeIndex(Math.min(previousIndex + 1, mIndexMax), device, flags);
+ break;
+ case AudioManager.ADJUST_LOWER:
+ // For stream, ADJUST_LOWER on a muted VSS is a no-op
+ // If we decide to unmute on ADJUST_LOWER, cannot fallback on
+ // adjustStreamVolume for group associated to legacy stream type
+ if (isMuted() && previousIndex != 0) {
+ mute(false);
+ } else {
+ int newIndex = Math.max(previousIndex - 1, mIndexMin);
+ setVolumeIndex(newIndex, device, flags);
+ }
+ break;
+ }
+ }
+ }
+
public int getVolumeIndex() {
- return getIndex(getDeviceForVolume());
+ synchronized (VolumeStreamState.class) {
+ return getIndex(getDeviceForVolume());
+ }
}
public void setVolumeIndex(int index, int flags) {
- if (mUseFixedVolume) {
- return;
+ synchronized (VolumeStreamState.class) {
+ if (mUseFixedVolume) {
+ return;
+ }
+ setVolumeIndex(index, getDeviceForVolume(), flags);
}
- setVolumeIndex(index, getDeviceForVolume(), flags);
}
+ @GuardedBy("AudioService.VolumeStreamState.class")
private void setVolumeIndex(int index, int device, int flags) {
- // Set the volume index
- setVolumeIndexInt(index, device, flags);
-
- // Update local cache
- mIndexMap.put(device, index);
-
- // update data base - post a persist volume group msg
- sendMsg(mAudioHandler,
- MSG_PERSIST_VOLUME_GROUP,
- SENDMSG_QUEUE,
- device,
- 0,
- this,
- PERSIST_DELAY);
+ // Update cache & persist (muted by volume 0 shall be persisted)
+ updateVolumeIndex(index, device);
+ // setting non-zero volume for a muted stream unmutes the stream and vice versa,
+ boolean changed = mute(index == 0);
+ if (!changed) {
+ // Set the volume index only if mute operation is a no-op
+ index = getValidIndex(index);
+ setVolumeIndexInt(index, device, flags);
+ }
}
+ @GuardedBy("AudioService.VolumeStreamState.class")
+ public void updateVolumeIndex(int index, int device) {
+ // Filter persistency if already exist and the index has not changed
+ if (mIndexMap.indexOfKey(device) < 0 || mIndexMap.get(device) != index) {
+ // Update local cache
+ mIndexMap.put(device, getValidIndex(index));
+
+ // update data base - post a persist volume group msg
+ sendMsg(mAudioHandler,
+ MSG_PERSIST_VOLUME_GROUP,
+ SENDMSG_QUEUE,
+ device,
+ 0,
+ this,
+ PERSIST_DELAY);
+ }
+ }
+
+ @GuardedBy("AudioService.VolumeStreamState.class")
private void setVolumeIndexInt(int index, int device, int flags) {
// Reflect mute state of corresponding stream by forcing index to 0 if muted
// Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
// This allows RX path muting by the audio HAL only when explicitly muted but not when
// index is just set to 0 to repect BT requirements
- if (mStreamStates[mPublicStreamType].isFullyMuted()) {
+ if (mHasValidStreamType && isVssMuteBijective(mPublicStreamType)
+ && mStreamStates[mPublicStreamType].isFullyMuted()) {
index = 0;
} else if (mPublicStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0) {
index = 1;
@@ -7462,18 +7695,16 @@
AudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, device);
}
- public int getIndex(int device) {
- synchronized (VolumeGroupState.class) {
- int index = mIndexMap.get(device, -1);
- // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
- return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
- }
+ @GuardedBy("AudioService.VolumeStreamState.class")
+ private int getIndex(int device) {
+ int index = mIndexMap.get(device, -1);
+ // there is always an entry for AudioSystem.DEVICE_OUT_DEFAULT
+ return (index != -1) ? index : mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
}
- public boolean hasIndexForDevice(int device) {
- synchronized (VolumeGroupState.class) {
- return (mIndexMap.get(device, -1) != -1);
- }
+ @GuardedBy("AudioService.VolumeStreamState.class")
+ private boolean hasIndexForDevice(int device) {
+ return (mIndexMap.get(device, -1) != -1);
}
public int getMaxIndex() {
@@ -7484,55 +7715,108 @@
return mIndexMin;
}
- private boolean isValidLegacyStreamType() {
- return (mLegacyStreamType != AudioSystem.STREAM_DEFAULT)
- && (mLegacyStreamType < mStreamStates.length);
+ private boolean isValidStream(int stream) {
+ return (stream != AudioSystem.STREAM_DEFAULT) && (stream < mStreamStates.length);
}
- public void applyAllVolumes() {
- synchronized (VolumeGroupState.class) {
- int deviceForStream = AudioSystem.DEVICE_NONE;
- int volumeIndexForStream = 0;
- if (isValidLegacyStreamType()) {
- // Prevent to apply settings twice when group is associated to public stream
- deviceForStream = getDeviceForStream(mLegacyStreamType);
- volumeIndexForStream = getStreamVolume(mLegacyStreamType);
- }
+ public boolean isMusic() {
+ return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC;
+ }
+
+ public void applyAllVolumes(boolean userSwitch) {
+ String caller = "from vgs";
+ synchronized (VolumeStreamState.class) {
// apply device specific volumes first
- int index;
for (int i = 0; i < mIndexMap.size(); i++) {
- final int device = mIndexMap.keyAt(i);
+ int device = mIndexMap.keyAt(i);
+ int index = mIndexMap.valueAt(i);
+ boolean synced = false;
if (device != AudioSystem.DEVICE_OUT_DEFAULT) {
- index = mIndexMap.valueAt(i);
- if (device == deviceForStream && volumeIndexForStream == index) {
- continue;
+ for (int stream : getLegacyStreamTypes()) {
+ if (isValidStream(stream)) {
+ boolean streamMuted = mStreamStates[stream].mIsMuted;
+ int deviceForStream = getDeviceForStream(stream);
+ int indexForStream =
+ (mStreamStates[stream].getIndex(deviceForStream) + 5) / 10;
+ if (device == deviceForStream) {
+ if (indexForStream == index && (isMuted() == streamMuted)
+ && isVssMuteBijective(stream)) {
+ synced = true;
+ continue;
+ }
+ if (indexForStream != index) {
+ mStreamStates[stream].setIndex(index * 10, device, caller,
+ true /*hasModifyAudioSettings*/);
+ }
+ if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+ mStreamStates[stream].mute(isMuted());
+ }
+ }
+ }
}
- if (DEBUG_VOL) {
- Log.v(TAG, "applyAllVolumes: restore index " + index + " for group "
- + mAudioVolumeGroup.name() + " and device "
- + AudioSystem.getOutputDeviceName(device));
+ if (!synced) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "applyAllVolumes: apply index " + index + ", group "
+ + mAudioVolumeGroup.name() + " and device "
+ + AudioSystem.getOutputDeviceName(device));
+ }
+ setVolumeIndexInt(isMuted() ? 0 : index, device, 0 /*flags*/);
}
- setVolumeIndexInt(index, device, 0 /*flags*/);
}
}
// apply default volume last: by convention , default device volume will be used
// by audio policy manager if no explicit volume is present for a given device type
- index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
- if (DEBUG_VOL) {
- Log.v(TAG, "applyAllVolumes: restore default device index " + index
- + " for group " + mAudioVolumeGroup.name());
- }
- if (isValidLegacyStreamType()) {
- int defaultStreamIndex = (mStreamStates[mLegacyStreamType]
- .getIndex(AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10;
- if (defaultStreamIndex == index) {
- return;
+ int index = getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
+ boolean synced = false;
+ int deviceForVolume = getDeviceForVolume();
+ boolean forceDeviceSync = userSwitch && (mIndexMap.indexOfKey(deviceForVolume) < 0);
+ for (int stream : getLegacyStreamTypes()) {
+ if (isValidStream(stream)) {
+ boolean streamMuted = mStreamStates[stream].mIsMuted;
+ int defaultStreamIndex = (mStreamStates[stream].getIndex(
+ AudioSystem.DEVICE_OUT_DEFAULT) + 5) / 10;
+ if (forceDeviceSync) {
+ mStreamStates[stream].setIndex(index * 10, deviceForVolume, caller,
+ true /*hasModifyAudioSettings*/);
+ }
+ if (defaultStreamIndex == index && (isMuted() == streamMuted)
+ && isVssMuteBijective(stream)) {
+ synced = true;
+ continue;
+ }
+ if (defaultStreamIndex != index) {
+ mStreamStates[stream].setIndex(
+ index * 10, AudioSystem.DEVICE_OUT_DEFAULT, caller,
+ true /*hasModifyAudioSettings*/);
+ }
+ if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+ mStreamStates[stream].mute(isMuted());
+ }
}
}
- setVolumeIndexInt(index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/);
+ if (!synced) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "applyAllVolumes: apply default device index " + index
+ + ", group " + mAudioVolumeGroup.name());
+ }
+ setVolumeIndexInt(
+ isMuted() ? 0 : index, AudioSystem.DEVICE_OUT_DEFAULT, 0 /*flags*/);
+ }
+ if (forceDeviceSync) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "applyAllVolumes: forceDeviceSync index " + index
+ + ", device " + AudioSystem.getOutputDeviceName(deviceForVolume)
+ + ", group " + mAudioVolumeGroup.name());
+ }
+ setVolumeIndexInt(isMuted() ? 0 : index, deviceForVolume, 0);
+ }
}
}
+ public void clearIndexCache() {
+ mIndexMap.clear();
+ }
+
private void persistVolumeGroup(int device) {
if (mUseFixedVolume) {
return;
@@ -7541,21 +7825,19 @@
Log.v(TAG, "persistVolumeGroup: storing index " + getIndex(device) + " for group "
+ mAudioVolumeGroup.name()
+ ", device " + AudioSystem.getOutputDeviceName(device)
- + " and User=" + ActivityManager.getCurrentUser());
+ + " and User=" + getCurrentUserId());
}
boolean success = mSettings.putSystemIntForUser(mContentResolver,
getSettingNameForDevice(device),
getIndex(device),
- UserHandle.USER_CURRENT);
+ isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
if (!success) {
Log.e(TAG, "persistVolumeGroup failed for group " + mAudioVolumeGroup.name());
}
}
public void readSettings() {
- synchronized (VolumeGroupState.class) {
- // First clear previously loaded (previous user?) settings
- mIndexMap.clear();
+ synchronized (VolumeStreamState.class) {
// force maximum volume on all streams if fixed volume property is set
if (mUseFixedVolume) {
mIndexMap.put(AudioSystem.DEVICE_OUT_DEFAULT, mIndexMax);
@@ -7570,7 +7852,8 @@
int index;
String name = getSettingNameForDevice(device);
index = mSettings.getSystemIntForUser(
- mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT);
+ mContentResolver, name, defaultIndex,
+ isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
if (index == -1) {
continue;
}
@@ -7581,13 +7864,14 @@
if (DEBUG_VOL) {
Log.v(TAG, "readSettings: found stored index " + getValidIndex(index)
+ " for group " + mAudioVolumeGroup.name() + ", device: " + name
- + ", User=" + ActivityManager.getCurrentUser());
+ + ", User=" + getCurrentUserId());
}
mIndexMap.put(device, getValidIndex(index));
}
}
}
+ @GuardedBy("AudioService.VolumeStreamState.class")
private int getValidIndex(int index) {
if (index < mIndexMin) {
return mIndexMin;
@@ -7598,15 +7882,17 @@
}
public @NonNull String getSettingNameForDevice(int device) {
- final String suffix = AudioSystem.getOutputDeviceName(device);
+ String suffix = AudioSystem.getOutputDeviceName(device);
if (suffix.isEmpty()) {
- return mAudioVolumeGroup.name();
+ return mSettingName;
}
- return mAudioVolumeGroup.name() + "_" + AudioSystem.getOutputDeviceName(device);
+ return mSettingName + "_" + AudioSystem.getOutputDeviceName(device);
}
private void dump(PrintWriter pw) {
pw.println("- VOLUME GROUP " + mAudioVolumeGroup.name() + ":");
+ pw.print(" Muted: ");
+ pw.println(mIsMuted);
pw.print(" Min: ");
pw.println(mIndexMin);
pw.print(" Max: ");
@@ -7616,9 +7902,9 @@
if (i > 0) {
pw.print(", ");
}
- final int device = mIndexMap.keyAt(i);
+ int device = mIndexMap.keyAt(i);
pw.print(Integer.toHexString(device));
- final String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
+ String deviceName = device == AudioSystem.DEVICE_OUT_DEFAULT ? "default"
: AudioSystem.getOutputDeviceName(device);
if (!deviceName.isEmpty()) {
pw.print(" (");
@@ -7631,7 +7917,7 @@
pw.println();
pw.print(" Devices: ");
int n = 0;
- final int devices = getDeviceForVolume();
+ int devices = getDeviceForVolume();
for (int device : AudioSystem.DEVICE_OUT_ALL_SET) {
if ((devices & device) == device) {
if (n++ > 0) {
@@ -7640,6 +7926,10 @@
pw.print(AudioSystem.getOutputDeviceName(device));
}
}
+ pw.println();
+ pw.print(" Streams: ");
+ Arrays.stream(getLegacyStreamTypes())
+ .forEach(stream -> pw.print(AudioSystem.streamToString(stream) + " "));
}
}
@@ -7651,13 +7941,14 @@
// 4 VolumeStreamState.class
private class VolumeStreamState {
private final int mStreamType;
+ private VolumeGroupState mVolumeGroupState = null;
private int mIndexMin;
// min index when user doesn't have permission to change audio settings
private int mIndexMinNoPerm;
private int mIndexMax;
- private boolean mIsMuted;
- private boolean mIsMutedInternally;
+ private boolean mIsMuted = false;
+ private boolean mIsMutedInternally = false;
private String mVolumeIndexSettingName;
@NonNull private Set<Integer> mObservedDeviceSet = new TreeSet<>();
@@ -7715,6 +8006,15 @@
}
/**
+ * Associate a {@link volumeGroupState} on the {@link VolumeStreamState}.
+ * <p> It helps to synchronize the index, mute attributes on the maching
+ * {@link volumeGroupState}
+ * @param volumeGroupState matching the {@link VolumeStreamState}
+ */
+ public void setVolumeGroupState(VolumeGroupState volumeGroupState) {
+ mVolumeGroupState = volumeGroupState;
+ }
+ /**
* Update the minimum index that can be used without MODIFY_AUDIO_SETTINGS permission
* @param index minimum index expressed in "UI units", i.e. no 10x factor
*/
@@ -7972,6 +8272,9 @@
}
}
if (changed) {
+ // If associated to volume group, update group cache
+ updateVolumeGroupIndex(device, /* forceMuteState= */ false);
+
oldIndex = (oldIndex + 5) / 10;
index = (index + 5) / 10;
// log base stream changes to the event log
@@ -7989,6 +8292,8 @@
mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
mStreamVolumeAlias[mStreamType]);
+ AudioService.sVolumeLogger.log(new VolChangedBroadcastEvent(
+ mStreamType, mStreamVolumeAlias[mStreamType], index));
sendBroadcastToAll(mVolumeChanged);
}
}
@@ -8072,6 +8377,28 @@
}
}
+ // If associated to volume group, update group cache
+ private void updateVolumeGroupIndex(int device, boolean forceMuteState) {
+ synchronized (VolumeStreamState.class) {
+ if (mVolumeGroupState != null) {
+ int groupIndex = (getIndex(device) + 5) / 10;
+ if (DEBUG_VOL) {
+ Log.d(TAG, "updateVolumeGroupIndex for stream " + mStreamType
+ + ", muted=" + mIsMuted + ", device=" + device + ", index="
+ + getIndex(device) + ", group " + mVolumeGroupState.name()
+ + " Muted=" + mVolumeGroupState.isMuted() + ", Index=" + groupIndex
+ + ", forceMuteState=" + forceMuteState);
+ }
+ mVolumeGroupState.updateVolumeIndex(groupIndex, device);
+ // Only propage mute of stream when applicable
+ if (mIndexMin == 0 || isCallStream(mStreamType)) {
+ // For call stream, align mute only when muted, not when index is set to 0
+ mVolumeGroupState.mute(forceMuteState ? mIsMuted : groupIndex == 0);
+ }
+ }
+ }
+ }
+
/**
* Mute/unmute the stream
* @param state the new mute state
@@ -8080,27 +8407,10 @@
public boolean mute(boolean state) {
boolean changed = false;
synchronized (VolumeStreamState.class) {
- if (state != mIsMuted) {
- changed = true;
- mIsMuted = state;
-
- // Set the new mute volume. This propagates the values to
- // the audio system, otherwise the volume won't be changed
- // at the lower level.
- sendMsg(mAudioHandler,
- MSG_SET_ALL_VOLUMES,
- SENDMSG_QUEUE,
- 0,
- 0,
- this, 0);
- }
+ changed = mute(state, true);
}
if (changed) {
- // Stream mute changed, fire the intent.
- Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
- intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType);
- intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, state);
- sendBroadcastToAll(intent);
+ broadcastMuteSetting(mStreamType, state);
}
return changed;
}
@@ -8132,6 +8442,44 @@
return mIsMuted || mIsMutedInternally;
}
+ /**
+ * Mute/unmute the stream
+ * @param state the new mute state
+ * @param apply true to propagate to HW, or false just to update the cache. May be needed
+ * to mute a stream and its aliases as applyAllVolume will force settings to aliases.
+ * It prevents unnecessary calls to {@see AudioSystem#setStreamVolume}
+ * @return true if the mute state was changed
+ */
+ public boolean mute(boolean state, boolean apply) {
+ synchronized (VolumeStreamState.class) {
+ boolean changed = state != mIsMuted;
+ if (changed) {
+ mIsMuted = state;
+ if (apply) {
+ doMute();
+ }
+ }
+ return changed;
+ }
+ }
+
+ public void doMute() {
+ synchronized (VolumeStreamState.class) {
+ // If associated to volume group, update group cache
+ updateVolumeGroupIndex(getDeviceForStream(mStreamType), /* forceMuteState= */ true);
+
+ // Set the new mute volume. This propagates the values to
+ // the audio system, otherwise the volume won't be changed
+ // at the lower level.
+ sendMsg(mAudioHandler,
+ MSG_SET_ALL_VOLUMES,
+ SENDMSG_QUEUE,
+ 0,
+ 0,
+ this, 0);
+ }
+ }
+
public int getStreamType() {
return mStreamType;
}
@@ -8201,6 +8549,9 @@
pw.println();
pw.print(" Devices: ");
pw.print(AudioSystem.deviceSetToString(getDeviceSetForStream(mStreamType)));
+ pw.println();
+ pw.print(" Volume Group: ");
+ pw.println(mVolumeGroupState != null ? mVolumeGroupState.name() : "n/a");
}
}
@@ -9867,7 +10218,7 @@
private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000;
private static final String ACTION_CHECK_MUSIC_ACTIVE =
- AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE";
+ "com.android.server.audio.action.CHECK_MUSIC_ACTIVE";
private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1;
private int safeMediaVolumeIndex(int device) {
@@ -9943,7 +10294,8 @@
mPendingVolumeCommand.mIndex,
mPendingVolumeCommand.mFlags,
mPendingVolumeCommand.mDevice,
- callingPackage, true /*hasModifyAudioSettings*/);
+ callingPackage, true /*hasModifyAudioSettings*/,
+ true /*canChangeMute*/);
mPendingVolumeCommand = null;
}
}
@@ -10162,7 +10514,7 @@
static final int LOG_NB_EVENTS_PHONE_STATE = 20;
static final int LOG_NB_EVENTS_DEVICE_CONNECTION = 50;
static final int LOG_NB_EVENTS_FORCE_USE = 20;
- static final int LOG_NB_EVENTS_VOLUME = 40;
+ static final int LOG_NB_EVENTS_VOLUME = 100;
static final int LOG_NB_EVENTS_DYN_POLICY = 10;
static final int LOG_NB_EVENTS_SPATIAL = 30;
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 30a9e0a7..6cbe03e 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -17,7 +17,6 @@
package com.android.server.audio;
import android.annotation.NonNull;
-import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
@@ -147,19 +146,42 @@
}
}
+ static final class VolChangedBroadcastEvent extends AudioEventLogger.Event {
+ final int mStreamType;
+ final int mAliasStreamType;
+ final int mIndex;
+
+ VolChangedBroadcastEvent(int stream, int alias, int index) {
+ mStreamType = stream;
+ mAliasStreamType = alias;
+ mIndex = index;
+ }
+
+ @Override
+ public String eventToString() {
+ return new StringBuilder("sending VOLUME_CHANGED stream:")
+ .append(AudioSystem.streamToString(mStreamType))
+ .append(" index:").append(mIndex)
+ .append(" alias:").append(AudioSystem.streamToString(mAliasStreamType))
+ .toString();
+ }
+ }
+
static final class DeviceVolumeEvent extends AudioEventLogger.Event {
final int mStream;
final int mVolIndex;
final String mDeviceNativeType;
final String mDeviceAddress;
final String mCaller;
+ final int mDeviceForStream;
DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device,
- String callingPackage) {
+ int deviceForStream, String callingPackage) {
mStream = streamType;
mVolIndex = index;
mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType());
mDeviceAddress = device.getAddress();
+ mDeviceForStream = deviceForStream;
mCaller = callingPackage;
// log metrics
new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT)
@@ -180,7 +202,9 @@
.append(" index:").append(mVolIndex)
.append(" device:").append(mDeviceNativeType)
.append(" addr:").append(mDeviceAddress)
- .append(") from ").append(mCaller).toString();
+ .append(") from ").append(mCaller)
+ .append(" currDevForStream:Ox").append(Integer.toHexString(mDeviceForStream))
+ .toString();
}
}
@@ -196,6 +220,7 @@
static final int VOL_SET_GROUP_VOL = 8;
static final int VOL_MUTE_STREAM_INT = 9;
static final int VOL_SET_LE_AUDIO_VOL = 10;
+ static final int VOL_ADJUST_GROUP_VOL = 11;
final int mOp;
final int mStream;
@@ -203,7 +228,6 @@
final int mVal2;
final String mCaller;
final String mGroupName;
- final AudioAttributes mAudioAttributes;
/** used for VOL_ADJUST_VOL_UID,
* VOL_ADJUST_SUGG_VOL,
@@ -216,7 +240,6 @@
mVal2 = val2;
mCaller = caller;
mGroupName = null;
- mAudioAttributes = null;
logMetricEvent();
}
@@ -229,7 +252,6 @@
mStream = -1;
mCaller = null;
mGroupName = null;
- mAudioAttributes = null;
logMetricEvent();
}
@@ -242,7 +264,6 @@
mStream = -1;
mCaller = null;
mGroupName = null;
- mAudioAttributes = null;
logMetricEvent();
}
@@ -255,7 +276,6 @@
// unused
mCaller = null;
mGroupName = null;
- mAudioAttributes = null;
logMetricEvent();
}
@@ -268,19 +288,18 @@
// unused
mCaller = null;
mGroupName = null;
- mAudioAttributes = null;
logMetricEvent();
}
- /** used for VOL_SET_GROUP_VOL */
- VolumeEvent(int op, AudioAttributes aa, String group, int index, int flags, String caller) {
+ /** used for VOL_SET_GROUP_VOL,
+ * VOL_ADJUST_GROUP_VOL */
+ VolumeEvent(int op, String group, int index, int flags, String caller) {
mOp = op;
mStream = -1;
mVal1 = index;
mVal2 = flags;
mCaller = caller;
mGroupName = group;
- mAudioAttributes = aa;
logMetricEvent();
}
@@ -292,7 +311,6 @@
mVal2 = 0;
mCaller = null;
mGroupName = null;
- mAudioAttributes = null;
logMetricEvent();
}
@@ -334,6 +352,15 @@
.record();
return;
}
+ case VOL_ADJUST_GROUP_VOL:
+ new MediaMetrics.Item(mMetricsId)
+ .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+ .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down")
+ .set(MediaMetrics.Property.EVENT, "adjustVolumeGroupVolume")
+ .set(MediaMetrics.Property.FLAGS, mVal2)
+ .set(MediaMetrics.Property.GROUP, mGroupName)
+ .record();
+ return;
case VOL_SET_STREAM_VOL:
new MediaMetrics.Item(mMetricsId)
.set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
@@ -385,7 +412,6 @@
return;
case VOL_SET_GROUP_VOL:
new MediaMetrics.Item(mMetricsId)
- .set(MediaMetrics.Property.ATTRIBUTES, mAudioAttributes.toString())
.set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
.set(MediaMetrics.Property.EVENT, "setVolumeIndexForAttributes")
.set(MediaMetrics.Property.FLAGS, mVal2)
@@ -411,6 +437,13 @@
.append(" flags:0x").append(Integer.toHexString(mVal2))
.append(") from ").append(mCaller)
.toString();
+ case VOL_ADJUST_GROUP_VOL:
+ return new StringBuilder("adjustVolumeGroupVolume(group:")
+ .append(mGroupName)
+ .append(" dir:").append(AudioManager.adjustToString(mVal1))
+ .append(" flags:0x").append(Integer.toHexString(mVal2))
+ .append(") from ").append(mCaller)
+ .toString();
case VOL_ADJUST_STREAM_VOL:
return new StringBuilder("adjustStreamVolume(stream:")
.append(AudioSystem.streamToString(mStream))
@@ -459,8 +492,7 @@
.append(" stream:").append(AudioSystem.streamToString(mStream))
.toString();
case VOL_SET_GROUP_VOL:
- return new StringBuilder("setVolumeIndexForAttributes(attr:")
- .append(mAudioAttributes.toString())
+ return new StringBuilder("setVolumeIndexForAttributes(group:")
.append(" group: ").append(mGroupName)
.append(" index:").append(mVal1)
.append(" flags:0x").append(Integer.toHexString(mVal2))
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index c3754eb..a17b4bf 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -29,8 +29,12 @@
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -60,8 +64,12 @@
private String[] mMethodNames = {"getDevicesForAttributes"};
private static final boolean USE_CACHE_FOR_GETDEVICES = true;
+ private static final Object sDeviceCacheLock = new Object();
+ @GuardedBy("sDeviceCacheLock")
private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
mDevicesForAttrCache;
+ @GuardedBy("sDeviceCacheLock")
+ private long mDevicesForAttributesCacheClearTimeMs = System.currentTimeMillis();
private int[] mMethodCacheHit;
private static final Object sRoutingListenerLock = new Object();
@GuardedBy("sRoutingListenerLock")
@@ -105,6 +113,13 @@
}
}
+ public void clearRoutingCache() {
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "---- routing cache clear (from java) ----------");
+ }
+ invalidateRoutingCache();
+ }
+
/**
* Implementation of AudioSystem.VolumeRangeInitRequestCallback
*/
@@ -140,9 +155,11 @@
AudioSystem.setRoutingCallback(sSingletonDefaultAdapter);
AudioSystem.setVolumeRangeInitRequestCallback(sSingletonDefaultAdapter);
if (USE_CACHE_FOR_GETDEVICES) {
- sSingletonDefaultAdapter.mDevicesForAttrCache =
- new ConcurrentHashMap<>(AudioSystem.getNumStreamTypes());
- sSingletonDefaultAdapter.mMethodCacheHit = new int[NB_MEASUREMENTS];
+ synchronized (sDeviceCacheLock) {
+ sSingletonDefaultAdapter.mDevicesForAttrCache =
+ new ConcurrentHashMap<>(AudioSystem.getNumStreamTypes());
+ sSingletonDefaultAdapter.mMethodCacheHit = new int[NB_MEASUREMENTS];
+ }
}
if (ENABLE_GETDEVICES_STATS) {
sSingletonDefaultAdapter.mMethodCallCounter = new int[NB_MEASUREMENTS];
@@ -156,8 +173,9 @@
if (DEBUG_CACHE) {
Log.d(TAG, "---- clearing cache ----------");
}
- if (mDevicesForAttrCache != null) {
- synchronized (mDevicesForAttrCache) {
+ synchronized (sDeviceCacheLock) {
+ if (mDevicesForAttrCache != null) {
+ mDevicesForAttributesCacheClearTimeMs = System.currentTimeMillis();
mDevicesForAttrCache.clear();
}
}
@@ -186,7 +204,7 @@
if (USE_CACHE_FOR_GETDEVICES) {
ArrayList<AudioDeviceAttributes> res;
final Pair<AudioAttributes, Boolean> key = new Pair(attributes, forVolume);
- synchronized (mDevicesForAttrCache) {
+ synchronized (sDeviceCacheLock) {
res = mDevicesForAttrCache.get(key);
if (res == null) {
// result from AudioSystem guaranteed non-null, but could be invalid
@@ -337,6 +355,7 @@
* @return
*/
public int setParameters(String keyValuePairs) {
+ invalidateRoutingCache();
return AudioSystem.setParameters(keyValuePairs);
}
@@ -500,23 +519,31 @@
*/
public void dump(PrintWriter pw) {
pw.println("\nAudioSystemAdapter:");
- pw.println(" mDevicesForAttrCache:");
- if (mDevicesForAttrCache != null) {
- for (Map.Entry<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
- entry : mDevicesForAttrCache.entrySet()) {
- final AudioAttributes attributes = entry.getKey().first;
- try {
- final int stream = attributes.getVolumeControlStream();
- pw.println("\t" + attributes + " forVolume: " + entry.getKey().second
- + " stream: "
- + AudioSystem.STREAM_NAMES[stream] + "(" + stream + ")");
- for (AudioDeviceAttributes devAttr : entry.getValue()) {
- pw.println("\t\t" + devAttr);
+ final DateTimeFormatter formatter = DateTimeFormatter
+ .ofPattern("MM-dd HH:mm:ss:SSS")
+ .withLocale(Locale.US)
+ .withZone(ZoneId.systemDefault());
+ synchronized (sDeviceCacheLock) {
+ pw.println(" last cache clear time: " + formatter.format(
+ Instant.ofEpochMilli(mDevicesForAttributesCacheClearTimeMs)));
+ pw.println(" mDevicesForAttrCache:");
+ if (mDevicesForAttrCache != null) {
+ for (Map.Entry<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
+ entry : mDevicesForAttrCache.entrySet()) {
+ final AudioAttributes attributes = entry.getKey().first;
+ try {
+ final int stream = attributes.getVolumeControlStream();
+ pw.println("\t" + attributes + " forVolume: " + entry.getKey().second
+ + " stream: "
+ + AudioSystem.STREAM_NAMES[stream] + "(" + stream + ")");
+ for (AudioDeviceAttributes devAttr : entry.getValue()) {
+ pw.println("\t\t" + devAttr);
+ }
+ } catch (IllegalArgumentException e) {
+ // dump could fail if attributes do not map to a stream.
+ pw.println("\t dump failed for attributes: " + attributes);
+ Log.e(TAG, "dump failed", e);
}
- } catch (IllegalArgumentException e) {
- // dump could fail if attributes do not map to a stream.
- pw.println("\t dump failed for attributes: " + attributes);
- Log.e(TAG, "dump failed", e);
}
}
}
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 54be4bb..1862942 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -58,13 +58,15 @@
public final class PlaybackActivityMonitor
implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer {
- public static final String TAG = "AudioService.PlaybackActivityMonitor";
+ public static final String TAG = "AS.PlayActivityMonitor";
/*package*/ static final boolean DEBUG = false;
/*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
/*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2;
/*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3;
+ /*package*/ static final int VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID = 4;
+ // ducking settings for a "normal duck" at -14dB
private static final VolumeShaper.Configuration DUCK_VSHAPE =
new VolumeShaper.Configuration.Builder()
.setId(VOLUME_SHAPER_SYSTEM_DUCK_ID)
@@ -78,6 +80,22 @@
.build();
private static final VolumeShaper.Configuration DUCK_ID =
new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_DUCK_ID);
+
+ // ducking settings for a "strong duck" at -35dB (attenuation factor of 0.017783)
+ private static final VolumeShaper.Configuration STRONG_DUCK_VSHAPE =
+ new VolumeShaper.Configuration.Builder()
+ .setId(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID)
+ .setCurve(new float[] { 0.f, 1.f } /* times */,
+ new float[] { 1.f, 0.017783f } /* volumes */)
+ .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME)
+ .setDuration(MediaFocusControl.getFocusRampTimeMs(
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .build()))
+ .build();
+ private static final VolumeShaper.Configuration STRONG_DUCK_ID =
+ new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID);
+
private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED =
new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY)
.createIfNeeded()
@@ -659,11 +677,23 @@
// add the players eligible for ducking to the list, and duck them
// (if apcsToDuck is empty, this will at least mark this uid as ducked, so when
// players of the same uid start, they will be ducked by DuckingManager.checkDuck())
- mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck);
+ mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck, reqCausesStrongDuck(winner));
}
return true;
}
+ private boolean reqCausesStrongDuck(FocusRequester requester) {
+ if (requester.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) {
+ return false;
+ }
+ final int reqUsage = requester.getAudioAttributes().getUsage();
+ if ((reqUsage == AudioAttributes.USAGE_ASSISTANT)
+ || (reqUsage == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)) {
+ return true;
+ }
+ return false;
+ }
+
@Override
public void restoreVShapedPlayers(@NonNull FocusRequester winner) {
if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); }
@@ -939,10 +969,11 @@
private static final class DuckingManager {
private final HashMap<Integer, DuckedApp> mDuckers = new HashMap<Integer, DuckedApp>();
- synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck) {
+ synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck,
+ boolean requestCausesStrongDuck) {
if (DEBUG) { Log.v(TAG, "DuckingManager: duckUid() uid:"+ uid); }
if (!mDuckers.containsKey(uid)) {
- mDuckers.put(uid, new DuckedApp(uid));
+ mDuckers.put(uid, new DuckedApp(uid, requestCausesStrongDuck));
}
final DuckedApp da = mDuckers.get(uid);
for (AudioPlaybackConfiguration apc : apcsToDuck) {
@@ -989,10 +1020,13 @@
private static final class DuckedApp {
private final int mUid;
+ /** determines whether ducking is done with DUCK_VSHAPE or STRONG_DUCK_VSHAPE */
+ private final boolean mUseStrongDuck;
private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>();
- DuckedApp(int uid) {
+ DuckedApp(int uid, boolean useStrongDuck) {
mUid = uid;
+ mUseStrongDuck = useStrongDuck;
}
void dump(PrintWriter pw) {
@@ -1013,9 +1047,9 @@
return;
}
try {
- sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG));
+ sEventLogger.log((new DuckEvent(apc, skipRamp, mUseStrongDuck)).printLog(TAG));
apc.getPlayerProxy().applyVolumeShaper(
- DUCK_VSHAPE,
+ mUseStrongDuck ? STRONG_DUCK_VSHAPE : DUCK_VSHAPE,
skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
mDuckedPlayers.add(piid);
} catch (Exception e) {
@@ -1031,7 +1065,7 @@
sEventLogger.log((new AudioEventLogger.StringEvent("unducking piid:"
+ piid)).printLog(TAG));
apc.getPlayerProxy().applyVolumeShaper(
- DUCK_ID,
+ mUseStrongDuck ? STRONG_DUCK_ID : DUCK_ID,
VolumeShaper.Operation.REVERSE);
} catch (Exception e) {
Log.e(TAG, "Error unducking player piid:" + piid + " uid:" + mUid, e);
@@ -1146,13 +1180,17 @@
}
static final class DuckEvent extends VolumeShaperEvent {
+ final boolean mUseStrongDuck;
+
@Override
String getVSAction() {
- return "ducking";
+ return mUseStrongDuck ? "ducking (strong)" : "ducking";
}
- DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) {
+ DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, boolean useStrongDuck)
+ {
super(apc, skipRamp);
+ mUseStrongDuck = useStrongDuck;
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
index 57ea812..1924f3c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java
@@ -96,7 +96,11 @@
@Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
Slog.d(TAG, "Remove onClientFinished: " + clientMonitor + ", success: " + success);
- mCallback.onClientFinished(InternalCleanupClient.this, success);
+ if (mUnknownHALTemplates.isEmpty()) {
+ mCallback.onClientFinished(InternalCleanupClient.this, success);
+ } else {
+ startCleanupUnknownHalTemplates();
+ }
}
};
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 05e83da..787bfb0 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,15 +16,11 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
-import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskStackListener;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
-import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
import android.hardware.biometrics.common.ICancellationSignal;
@@ -92,7 +88,6 @@
private long mSideFpsLastAcquireStartTime;
private Runnable mAuthSuccessRunnable;
private final Clock mClock;
- private boolean mDidFinishSfps;
FingerprintAuthenticationClient(
@NonNull Context context,
@@ -198,9 +193,8 @@
@Override
protected void handleLifecycleAfterAuth(boolean authenticated) {
- if (authenticated && !mDidFinishSfps) {
+ if (authenticated) {
mCallback.onClientFinished(this, true /* success */);
- mDidFinishSfps = true;
}
}
@@ -210,13 +204,11 @@
return false;
}
- public void handleAuthenticate(
+ @Override
+ public void onAuthenticated(
BiometricAuthenticator.Identifier identifier,
boolean authenticated,
ArrayList<Byte> token) {
- if (authenticated && mSensorProps.isAnySidefpsType()) {
- Slog.i(TAG, "(sideFPS): No power press detected, sending auth");
- }
super.onAuthenticated(identifier, authenticated, token);
if (authenticated) {
mState = STATE_STOPPED;
@@ -227,72 +219,11 @@
}
@Override
- public void onAuthenticated(
- BiometricAuthenticator.Identifier identifier,
- boolean authenticated,
- ArrayList<Byte> token) {
-
- mHandler.post(
- () -> {
- long delay = 0;
- if (authenticated && mSensorProps.isAnySidefpsType()) {
- delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
-
- if (mSideFpsLastAcquireStartTime != -1) {
- delay = Math.max(0,
- delay - (mClock.millis() - mSideFpsLastAcquireStartTime));
- }
-
- Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps "
- + "waiting for power until: " + delay + "ms");
- }
-
- if (mHandler.hasMessages(MESSAGE_FINGER_UP)) {
- Slog.i(TAG, "Finger up detected, sending auth");
- delay = 0;
- }
-
- mAuthSuccessRunnable =
- () -> handleAuthenticate(identifier, authenticated, token);
- mHandler.postDelayed(
- mAuthSuccessRunnable,
- MESSAGE_AUTH_SUCCESS,
- delay);
- });
- }
-
- @Override
public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) {
// For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off
// for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired
mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
super.onAcquired(acquiredInfo, vendorCode);
- if (mSensorProps.isAnySidefpsType()) {
- if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
- mSideFpsLastAcquireStartTime = mClock.millis();
- }
- final boolean shouldLookForVendor =
- mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR;
- final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage;
- final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage;
- final boolean ignorePowerPress =
- acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch);
-
- if (ignorePowerPress) {
- Slog.d(TAG, "(sideFPS) onFingerUp");
- mHandler.post(() -> {
- if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) {
- Slog.d(TAG, "(sideFPS) skipping wait for power");
- mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
- mHandler.post(mAuthSuccessRunnable);
- } else {
- mHandler.postDelayed(() -> {
- }, MESSAGE_FINGER_UP, mFingerUpIgnoresPower);
- }
- });
- }
- }
-
}
@Override
@@ -488,22 +419,5 @@
}
@Override
- public void onPowerPressed() {
- if (mSensorProps.isAnySidefpsType()) {
- Slog.i(TAG, "(sideFPS): onPowerPressed");
- mHandler.post(() -> {
- if (mDidFinishSfps) {
- return;
- }
- Slog.i(TAG, "(sideFPS): finishing auth");
- // Ignore auths after a power has been detected
- mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
- // Do not call onError() as that will send an additional callback to coex.
- mDidFinishSfps = true;
- onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
- stopHalOperation();
- mSensorOverlays.hide(getSensorId());
- });
- }
- }
+ public void onPowerPressed() { }
}
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index 9dd2f84..b9ca57e 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -76,6 +76,10 @@
return layout;
}
+ int size() {
+ return mLayoutMap.size();
+ }
+
private Layout createLayout(int state) {
if (mLayoutMap.contains(state)) {
Slog.e(TAG, "Attempted to create a second layout for state " + state);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index fa812c1..3d81044 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -402,6 +402,8 @@
private static final String STABLE_ID_SUFFIX_FORMAT = "id_%d";
private static final String NO_SUFFIX_FORMAT = "%d";
private static final long STABLE_FLAG = 1L << 62;
+ private static final int DEFAULT_PEAK_REFRESH_RATE = 0;
+ private static final int DEFAULT_REFRESH_RATE = 60;
private static final int DEFAULT_LOW_REFRESH_RATE = 60;
private static final int DEFAULT_HIGH_REFRESH_RATE = 0;
private static final int[] DEFAULT_BRIGHTNESS_THRESHOLDS = new int[]{};
@@ -570,17 +572,29 @@
* using higher refresh rates, even if display modes with higher refresh rates are available
* from hardware composer. Only has an effect if the value is non-zero.
*/
- private int mDefaultHighRefreshRate = DEFAULT_HIGH_REFRESH_RATE;
+ private int mDefaultPeakRefreshRate = DEFAULT_PEAK_REFRESH_RATE;
/**
* The default refresh rate for a given device. This value sets the higher default
* refresh rate. If the hardware composer on the device supports display modes with
* a higher refresh rate than the default value specified here, the framework may use those
* higher refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling
- * setFrameRate(). We have historically allowed fallback to mDefaultHighRefreshRate if
- * mDefaultLowRefreshRate is set to 0, but this is not supported anymore.
+ * setFrameRate(). We have historically allowed fallback to mDefaultPeakRefreshRate if
+ * mDefaultRefreshRate is set to 0, but this is not supported anymore.
*/
- private int mDefaultLowRefreshRate = DEFAULT_LOW_REFRESH_RATE;
+ private int mDefaultRefreshRate = DEFAULT_REFRESH_RATE;
+
+ /**
+ * Default refresh rate in the high zone defined by brightness and ambient thresholds.
+ * If non-positive, then the refresh rate is unchanged even if thresholds are configured.
+ */
+ private int mDefaultHighBlockingZoneRefreshRate = DEFAULT_HIGH_REFRESH_RATE;
+
+ /**
+ * Default refresh rate in the zone defined by brightness and ambient thresholds.
+ * If non-positive, then the refresh rate is unchanged even if thresholds are configured.
+ */
+ private int mDefaultLowBlockingZoneRefreshRate = DEFAULT_LOW_REFRESH_RATE;
/**
* The display uses different gamma curves for different refresh rates. It's hard for panel
@@ -1295,15 +1309,29 @@
/**
* @return Default peak refresh rate of the associated display
*/
- public int getDefaultHighRefreshRate() {
- return mDefaultHighRefreshRate;
+ public int getDefaultPeakRefreshRate() {
+ return mDefaultPeakRefreshRate;
}
/**
* @return Default refresh rate of the associated display
*/
- public int getDefaultLowRefreshRate() {
- return mDefaultLowRefreshRate;
+ public int getDefaultRefreshRate() {
+ return mDefaultRefreshRate;
+ }
+
+ /**
+ * @return Default refresh rate in the higher blocking zone of the associated display
+ */
+ public int getDefaultHighBlockingZoneRefreshRate() {
+ return mDefaultHighBlockingZoneRefreshRate;
+ }
+
+ /**
+ * @return Default refresh rate in the lower blocking zone of the associated display
+ */
+ public int getDefaultLowBlockingZoneRefreshRate() {
+ return mDefaultLowBlockingZoneRefreshRate;
}
/**
@@ -1441,8 +1469,10 @@
+ ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable
+ ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable
+ "\n"
- + ", mDefaultRefreshRate= " + mDefaultLowRefreshRate
- + ", mDefaultPeakRefreshRate= " + mDefaultHighRefreshRate
+ + ", mDefaultLowBlockingZoneRefreshRate= " + mDefaultLowBlockingZoneRefreshRate
+ + ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate
+ + ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate
+ + ", mDefaultRefreshRate= " + mDefaultRefreshRate
+ ", mLowDisplayBrightnessThresholds= "
+ Arrays.toString(mLowDisplayBrightnessThresholds)
+ ", mLowAmbientBrightnessThresholds= "
@@ -1756,10 +1786,31 @@
BlockingZoneConfig higherBlockingZoneConfig =
(refreshRateConfigs == null) ? null
: refreshRateConfigs.getHigherBlockingZoneConfigs();
+ loadPeakDefaultRefreshRate(refreshRateConfigs);
+ loadDefaultRefreshRate(refreshRateConfigs);
loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig);
loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig);
}
+ private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
+ if (refreshRateConfigs == null || refreshRateConfigs.getDefaultPeakRefreshRate() == null) {
+ mDefaultPeakRefreshRate = mContext.getResources().getInteger(
+ R.integer.config_defaultPeakRefreshRate);
+ } else {
+ mDefaultPeakRefreshRate =
+ refreshRateConfigs.getDefaultPeakRefreshRate().intValue();
+ }
+ }
+
+ private void loadDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
+ if (refreshRateConfigs == null || refreshRateConfigs.getDefaultRefreshRate() == null) {
+ mDefaultRefreshRate = mContext.getResources().getInteger(
+ R.integer.config_defaultRefreshRate);
+ } else {
+ mDefaultRefreshRate =
+ refreshRateConfigs.getDefaultRefreshRate().intValue();
+ }
+ }
/**
* Loads the refresh rate configurations pertaining to the upper blocking zones.
@@ -1784,10 +1835,10 @@
private void loadHigherBlockingZoneDefaultRefreshRate(
BlockingZoneConfig upperBlockingZoneConfig) {
if (upperBlockingZoneConfig == null) {
- mDefaultHighRefreshRate = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_defaultPeakRefreshRate);
+ mDefaultHighBlockingZoneRefreshRate = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_fixedRefreshRateInHighZone);
} else {
- mDefaultHighRefreshRate =
+ mDefaultHighBlockingZoneRefreshRate =
upperBlockingZoneConfig.getDefaultRefreshRate().intValue();
}
}
@@ -1799,10 +1850,10 @@
private void loadLowerBlockingZoneDefaultRefreshRate(
BlockingZoneConfig lowerBlockingZoneConfig) {
if (lowerBlockingZoneConfig == null) {
- mDefaultLowRefreshRate = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_defaultRefreshRate);
+ mDefaultLowBlockingZoneRefreshRate = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_defaultRefreshRateInZone);
} else {
- mDefaultLowRefreshRate =
+ mDefaultLowBlockingZoneRefreshRate =
lowerBlockingZoneConfig.getDefaultRefreshRate().intValue();
}
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 1864328..079bcb6 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -104,6 +104,7 @@
import android.provider.Settings;
import android.sysprop.DisplayProperties;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
import android.util.IntArray;
@@ -256,6 +257,13 @@
final SparseArray<Pair<IVirtualDevice, DisplayWindowPolicyController>>
mDisplayWindowPolicyControllers = new SparseArray<>();
+ /**
+ * Map of every display device {@link HighBrightnessModeMetadata}s indexed by
+ * {@link DisplayDevice#mUniqueId}.
+ */
+ public final ArrayMap<String, HighBrightnessModeMetadata> mHighBrightnessModeMetadataMap =
+ new ArrayMap<>();
+
// List of all currently registered display adapters.
private final ArrayList<DisplayAdapter> mDisplayAdapters = new ArrayList<DisplayAdapter>();
@@ -1535,6 +1543,7 @@
final int displayId = display.getDisplayIdLocked();
final boolean isDefault = displayId == Display.DEFAULT_DISPLAY;
configureColorModeLocked(display, device);
+
if (!mAreUserDisabledHdrTypesAllowed) {
display.setUserDisabledHdrTypes(mUserDisabledHdrTypes);
}
@@ -1588,7 +1597,16 @@
DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
if (dpc != null) {
- dpc.onDisplayChanged();
+ final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+ if (device == null) {
+ Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: "
+ + display.getDisplayIdLocked());
+ return;
+ }
+
+ final String uniqueId = device.getUniqueId();
+ HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId);
+ dpc.onDisplayChanged(hbmMetadata);
}
}
@@ -1645,7 +1663,15 @@
final int displayId = display.getDisplayIdLocked();
final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId);
if (dpc != null) {
- dpc.onDisplayChanged();
+ final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+ if (device == null) {
+ Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: "
+ + display.getDisplayIdLocked());
+ return;
+ }
+ final String uniqueId = device.getUniqueId();
+ HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId);
+ dpc.onDisplayChanged(hbmMetadata);
}
}
@@ -2631,6 +2657,27 @@
mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked);
}
+ @VisibleForTesting
+ HighBrightnessModeMetadata getHighBrightnessModeMetadata(LogicalDisplay display) {
+ final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
+ if (device == null) {
+ Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: "
+ + display.getDisplayIdLocked());
+ return null;
+ }
+
+ final String uniqueId = device.getUniqueId();
+
+ if (mHighBrightnessModeMetadataMap.containsKey(uniqueId)) {
+ return mHighBrightnessModeMetadataMap.get(uniqueId);
+ }
+
+ // HBM Time info not present. Create a new one for this physical display.
+ HighBrightnessModeMetadata hbmInfo = new HighBrightnessModeMetadata();
+ mHighBrightnessModeMetadataMap.put(uniqueId, hbmInfo);
+ return hbmInfo;
+ }
+
private void addDisplayPowerControllerLocked(LogicalDisplay display) {
if (mPowerHandler == null) {
// initPowerManagement has not yet been called.
@@ -2642,10 +2689,18 @@
final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore,
display, mSyncRoot);
+
+ // If display already has a HighBrightnessModeMetadata mapping, use that.
+ // Or create a new one and use that.
+ // We also need to pass a mapping of the HighBrightnessModeTimeInfoMap to
+ // displayPowerController, so the hbm info can be correctly associated
+ // with the corresponding displaydevice.
+ HighBrightnessModeMetadata hbmMetadata = getHighBrightnessModeMetadata(display);
+
final DisplayPowerController displayPowerController = new DisplayPowerController(
mContext, mDisplayPowerCallbacks, mPowerHandler, mSensorManager,
mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
- () -> handleBrightnessChange(display));
+ () -> handleBrightnessChange(display), hbmMetadata);
mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController);
}
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index aafba5a..fdfc20a 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -1169,7 +1169,7 @@
mDefaultRefreshRate =
(displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
R.integer.config_defaultRefreshRate)
- : (float) displayDeviceConfig.getDefaultLowRefreshRate();
+ : (float) displayDeviceConfig.getDefaultRefreshRate();
}
public void observe() {
@@ -1256,7 +1256,7 @@
defaultPeakRefreshRate =
(displayDeviceConfig == null) ? (float) mContext.getResources().getInteger(
R.integer.config_defaultPeakRefreshRate)
- : (float) displayDeviceConfig.getDefaultHighRefreshRate();
+ : (float) displayDeviceConfig.getDefaultPeakRefreshRate();
}
mDefaultPeakRefreshRate = defaultPeakRefreshRate;
}
@@ -1612,8 +1612,26 @@
return mHighAmbientBrightnessThresholds;
}
+ /**
+ * @return the refresh rate to lock to when in a high brightness zone
+ */
+ @VisibleForTesting
+ int getRefreshRateInHighZone() {
+ return mRefreshRateInHighZone;
+ }
+
+ /**
+ * @return the refresh rate to lock to when in a low brightness zone
+ */
+ @VisibleForTesting
+ int getRefreshRateInLowZone() {
+ return mRefreshRateInLowZone;
+ }
+
private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
boolean attemptLoadingFromDeviceConfig) {
+ loadRefreshRateInHighZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
+ loadRefreshRateInLowZone(displayDeviceConfig, attemptLoadingFromDeviceConfig);
mLowDisplayBrightnessThresholds = loadBrightnessThresholds(
() -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(),
() -> displayDeviceConfig.getLowDisplayBrightnessThresholds(),
@@ -1634,6 +1652,44 @@
}
}
+ private void loadRefreshRateInLowZone(DisplayDeviceConfig displayDeviceConfig,
+ boolean attemptLoadingFromDeviceConfig) {
+ int refreshRateInLowZone =
+ (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+ R.integer.config_defaultRefreshRateInZone)
+ : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate();
+ if (attemptLoadingFromDeviceConfig) {
+ try {
+ refreshRateInLowZone = mDeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+ DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
+ refreshRateInLowZone);
+ } catch (Exception exception) {
+ // Do nothing
+ }
+ }
+ mRefreshRateInLowZone = refreshRateInLowZone;
+ }
+
+ private void loadRefreshRateInHighZone(DisplayDeviceConfig displayDeviceConfig,
+ boolean attemptLoadingFromDeviceConfig) {
+ int refreshRateInHighZone =
+ (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+ R.integer.config_fixedRefreshRateInHighZone) : displayDeviceConfig
+ .getDefaultHighBlockingZoneRefreshRate();
+ if (attemptLoadingFromDeviceConfig) {
+ try {
+ refreshRateInHighZone = mDeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+ DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
+ refreshRateInHighZone);
+ } catch (Exception exception) {
+ // Do nothing
+ }
+ }
+ mRefreshRateInHighZone = refreshRateInHighZone;
+ }
+
private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig,
boolean attemptLoadingFromDeviceConfig) {
mHighDisplayBrightnessThresholds = loadBrightnessThresholds(
@@ -1687,14 +1743,6 @@
}
/**
- * @return the refresh to lock to when in a low brightness zone
- */
- @VisibleForTesting
- int getRefreshRateInLowZone() {
- return mRefreshRateInLowZone;
- }
-
- /**
* @return the display brightness thresholds for the low brightness zones
*/
@VisibleForTesting
@@ -1739,8 +1787,17 @@
mHighAmbientBrightnessThresholds = highAmbientBrightnessThresholds;
}
- mRefreshRateInLowZone = mDeviceConfigDisplaySettings.getRefreshRateInLowZone();
- mRefreshRateInHighZone = mDeviceConfigDisplaySettings.getRefreshRateInHighZone();
+ final int refreshRateInLowZone = mDeviceConfigDisplaySettings
+ .getRefreshRateInLowZone();
+ if (refreshRateInLowZone != -1) {
+ mRefreshRateInLowZone = refreshRateInLowZone;
+ }
+
+ final int refreshRateInHighZone = mDeviceConfigDisplaySettings
+ .getRefreshRateInHighZone();
+ if (refreshRateInHighZone != -1) {
+ mRefreshRateInHighZone = refreshRateInHighZone;
+ }
restartObserver();
mDeviceConfigDisplaySettings.startListening();
@@ -1794,6 +1851,10 @@
restartObserver();
}
+ /**
+ * Used to reload the lower blocking zone refresh rate in case of changes in the
+ * DeviceConfig properties.
+ */
public void onDeviceConfigRefreshRateInLowZoneChanged(int refreshRate) {
if (refreshRate != mRefreshRateInLowZone) {
mRefreshRateInLowZone = refreshRate;
@@ -1817,6 +1878,10 @@
restartObserver();
}
+ /**
+ * Used to reload the higher blocking zone refresh rate in case of changes in the
+ * DeviceConfig properties.
+ */
public void onDeviceConfigRefreshRateInHighZoneChanged(int refreshRate) {
if (refreshRate != mRefreshRateInHighZone) {
mRefreshRateInHighZone = refreshRate;
@@ -2664,15 +2729,10 @@
}
public int getRefreshRateInLowZone() {
- int defaultRefreshRateInZone = mContext.getResources().getInteger(
- R.integer.config_defaultRefreshRateInZone);
-
- int refreshRate = mDeviceConfig.getInt(
+ return mDeviceConfig.getInt(
DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
- DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE,
- defaultRefreshRateInZone);
+ DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1);
- return refreshRate;
}
/*
@@ -2694,15 +2754,10 @@
}
public int getRefreshRateInHighZone() {
- int defaultRefreshRateInZone = mContext.getResources().getInteger(
- R.integer.config_fixedRefreshRateInHighZone);
-
- int refreshRate = mDeviceConfig.getInt(
+ return mDeviceConfig.getInt(
DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE,
- defaultRefreshRateInZone);
-
- return refreshRate;
+ -1);
}
public int getRefreshRateInHbmSunlight() {
@@ -2750,23 +2805,29 @@
int[] lowDisplayBrightnessThresholds = getLowDisplayBrightnessThresholds();
int[] lowAmbientBrightnessThresholds = getLowAmbientBrightnessThresholds();
- int refreshRateInLowZone = getRefreshRateInLowZone();
+ final int refreshRateInLowZone = getRefreshRateInLowZone();
mHandler.obtainMessage(MSG_LOW_BRIGHTNESS_THRESHOLDS_CHANGED,
new Pair<>(lowDisplayBrightnessThresholds, lowAmbientBrightnessThresholds))
.sendToTarget();
- mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone, 0)
+
+ if (refreshRateInLowZone != -1) {
+ mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone)
.sendToTarget();
+ }
int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds();
int[] highAmbientBrightnessThresholds = getHighAmbientBrightnessThresholds();
- int refreshRateInHighZone = getRefreshRateInHighZone();
+ final int refreshRateInHighZone = getRefreshRateInHighZone();
mHandler.obtainMessage(MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED,
new Pair<>(highDisplayBrightnessThresholds, highAmbientBrightnessThresholds))
.sendToTarget();
- mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone, 0)
+
+ if (refreshRateInHighZone != -1) {
+ mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone)
.sendToTarget();
+ }
final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight();
mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index b737317..eeb261c 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -391,6 +391,7 @@
private float[] mNitsRange;
private final HighBrightnessModeController mHbmController;
+ private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
private final BrightnessThrottler mBrightnessThrottler;
@@ -511,7 +512,7 @@
DisplayPowerCallbacks callbacks, Handler handler,
SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay,
BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
- Runnable onBrightnessChangeRunnable) {
+ Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata) {
mLogicalDisplay = logicalDisplay;
mDisplayId = mLogicalDisplay.getDisplayIdLocked();
final String displayIdStr = "[" + mDisplayId + "]";
@@ -521,6 +522,7 @@
mSuspendBlockerIdProxPositive = displayIdStr + "prox positive";
mSuspendBlockerIdProxNegative = displayIdStr + "prox negative";
mSuspendBlockerIdProxDebounce = displayIdStr + "prox debounce";
+ mHighBrightnessModeMetadata = hbmMetadata;
mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
@@ -793,7 +795,7 @@
* of each display need to be properly reflected in AutomaticBrightnessController.
*/
@GuardedBy("DisplayManagerService.mSyncRoot")
- public void onDisplayChanged() {
+ public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata) {
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
if (device == null) {
Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: "
@@ -815,11 +817,11 @@
mUniqueDisplayId = uniqueId;
mDisplayStatsId = mUniqueDisplayId.hashCode();
mDisplayDeviceConfig = config;
- loadFromDisplayDeviceConfig(token, info);
+ loadFromDisplayDeviceConfig(token, info, hbmMetadata);
- // Since the underlying display-device changed, we really don't know the
- // last command that was sent to change it's state. Lets assume it is off and we
- // trigger a change immediately.
+ /// Since the underlying display-device changed, we really don't know the
+ // last command that was sent to change it's state. Lets assume it is unknown so
+ // that we trigger a change immediately.
mPowerState.resetScreenState();
}
if (mIsEnabled != isEnabled || mIsInTransition != isInTransition) {
@@ -872,7 +874,8 @@
}
}
- private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info) {
+ private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
+ HighBrightnessModeMetadata hbmMetadata) {
// All properties that depend on the associated DisplayDevice and the DDC must be
// updated here.
loadBrightnessRampRates();
@@ -885,6 +888,7 @@
mBrightnessRampIncreaseMaxTimeMillis,
mBrightnessRampDecreaseMaxTimeMillis);
}
+ mHbmController.setHighBrightnessModeMetadata(hbmMetadata);
mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId,
mDisplayDeviceConfig.getHighBrightnessModeData(),
new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
@@ -1965,7 +1969,7 @@
if (mAutomaticBrightnessController != null) {
mAutomaticBrightnessController.update();
}
- }, mContext);
+ }, mHighBrightnessModeMetadata, mContext);
}
private BrightnessThrottler createBrightnessThrottlerLocked() {
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index 7d1396d..2c257a1 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -340,20 +340,12 @@
}
/**
- * Resets the screen state to {@link Display#STATE_OFF}. Even though we do not know the last
- * state that was sent to the underlying display-device, we assume it is off.
- *
- * We do not set the screen state to {@link Display#STATE_UNKNOWN} to avoid getting in the state
- * where PhotonicModulator holds onto the lock. This happens because we currently try to keep
- * the mScreenState and mPendingState in sync, however if the screenState is set to
- * {@link Display#STATE_UNKNOWN} here, mPendingState will get progressed to this, which will
- * force the PhotonicModulator thread to wait onto the lock to take it out of that state.
- * b/262294651 for more info.
+ * Resets the screen state to unknown. Useful when the underlying display-device changes for the
+ * LogicalDisplay and we do not know the last state that was sent to it.
*/
void resetScreenState() {
- mScreenState = Display.STATE_OFF;
+ mScreenState = Display.STATE_UNKNOWN;
mScreenReady = false;
- scheduleScreenUpdate();
}
private void scheduleScreenUpdate() {
@@ -514,6 +506,8 @@
boolean valid = state != Display.STATE_UNKNOWN && !Float.isNaN(brightnessState);
boolean changed = stateChanged || backlightChanged;
if (!valid || !changed) {
+ mStateChangeInProgress = false;
+ mBacklightChangeInProgress = false;
try {
mLock.wait();
} catch (InterruptedException ex) {
diff --git a/services/core/java/com/android/server/display/HbmEvent.java b/services/core/java/com/android/server/display/HbmEvent.java
new file mode 100644
index 0000000..5675e2f
--- /dev/null
+++ b/services/core/java/com/android/server/display/HbmEvent.java
@@ -0,0 +1,46 @@
+/*
+ * 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.server.display;
+
+
+/**
+ * Represents an event in which High Brightness Mode was enabled.
+ */
+class HbmEvent {
+ private long mStartTimeMillis;
+ private long mEndTimeMillis;
+
+ HbmEvent(long startTimeMillis, long endTimeMillis) {
+ this.mStartTimeMillis = startTimeMillis;
+ this.mEndTimeMillis = endTimeMillis;
+ }
+
+ public long getStartTimeMillis() {
+ return mStartTimeMillis;
+ }
+
+ public long getEndTimeMillis() {
+ return mEndTimeMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "HbmEvent: {startTimeMillis:" + mStartTimeMillis + ", endTimeMillis: "
+ + mEndTimeMillis + "}, total: "
+ + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]";
+ }
+}
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 0b9d4de..ac32d53 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -42,8 +42,8 @@
import com.android.server.display.DisplayManagerService.Clock;
import java.io.PrintWriter;
+import java.util.ArrayDeque;
import java.util.Iterator;
-import java.util.LinkedList;
/**
* Controls the status of high-brightness mode for devices that support it. This class assumes that
@@ -105,30 +105,24 @@
private int mHbmStatsState = FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF;
/**
- * If HBM is currently running, this is the start time for the current HBM session.
+ * If HBM is currently running, this is the start time and set of all events,
+ * for the current HBM session.
*/
- private long mRunningStartTimeMillis = -1;
-
- /**
- * List of previous HBM-events ordered from most recent to least recent.
- * Meant to store only the events that fall into the most recent
- * {@link mHbmData.timeWindowMillis}.
- */
- private LinkedList<HbmEvent> mEvents = new LinkedList<>();
+ private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null;
HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
String displayUniqueId, float brightnessMin, float brightnessMax,
HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
- Runnable hbmChangeCallback, Context context) {
+ Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) {
this(new Injector(), handler, width, height, displayToken, displayUniqueId, brightnessMin,
- brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, context);
+ brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, hbmMetadata, context);
}
@VisibleForTesting
HighBrightnessModeController(Injector injector, Handler handler, int width, int height,
IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax,
HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
- Runnable hbmChangeCallback, Context context) {
+ Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) {
mInjector = injector;
mContext = context;
mClock = injector.getClock();
@@ -137,6 +131,7 @@
mBrightnessMin = brightnessMin;
mBrightnessMax = brightnessMax;
mHbmChangeCallback = hbmChangeCallback;
+ mHighBrightnessModeMetadata = hbmMetadata;
mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
mSettingsObserver = new SettingsObserver(mHandler);
mRecalcRunnable = this::recalculateTimeAllowance;
@@ -222,19 +217,22 @@
// If we are starting or ending a high brightness mode session, store the current
// session in mRunningStartTimeMillis, or the old one in mEvents.
- final boolean wasHbmDrainingAvailableTime = mRunningStartTimeMillis != -1;
+ final long runningStartTime = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+ final boolean wasHbmDrainingAvailableTime = runningStartTime != -1;
final boolean shouldHbmDrainAvailableTime = mBrightness > mHbmData.transitionPoint
&& !mIsHdrLayerPresent;
if (wasHbmDrainingAvailableTime != shouldHbmDrainAvailableTime) {
final long currentTime = mClock.uptimeMillis();
if (shouldHbmDrainAvailableTime) {
- mRunningStartTimeMillis = currentTime;
+ mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime);
} else {
- mEvents.addFirst(new HbmEvent(mRunningStartTimeMillis, currentTime));
- mRunningStartTimeMillis = -1;
+ final HbmEvent hbmEvent = new HbmEvent(runningStartTime, currentTime);
+ mHighBrightnessModeMetadata.addHbmEvent(hbmEvent);
+ mHighBrightnessModeMetadata.setRunningStartTimeMillis(-1);
if (DEBUG) {
- Slog.d(TAG, "New HBM event: " + mEvents.getFirst());
+ Slog.d(TAG, "New HBM event: "
+ + mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst());
}
}
}
@@ -260,6 +258,10 @@
mSettingsObserver.stopObserving();
}
+ void setHighBrightnessModeMetadata(HighBrightnessModeMetadata hbmInfo) {
+ mHighBrightnessModeMetadata = hbmInfo;
+ }
+
void resetHbmData(int width, int height, IBinder displayToken, String displayUniqueId,
HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg) {
mWidth = width;
@@ -316,20 +318,22 @@
pw.println(" mBrightnessMax=" + mBrightnessMax);
pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis()));
pw.println(" mIsTimeAvailable= " + mIsTimeAvailable);
- pw.println(" mRunningStartTimeMillis=" + TimeUtils.formatUptime(mRunningStartTimeMillis));
+ pw.println(" mRunningStartTimeMillis="
+ + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
pw.println(" mIsThermalStatusWithinLimit=" + mIsThermalStatusWithinLimit);
pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
pw.println(" width*height=" + mWidth + "*" + mHeight);
pw.println(" mEvents=");
final long currentTime = mClock.uptimeMillis();
long lastStartTime = currentTime;
- if (mRunningStartTimeMillis != -1) {
- lastStartTime = dumpHbmEvent(pw, new HbmEvent(mRunningStartTimeMillis, currentTime));
+ long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+ if (runningStartTimeMillis != -1) {
+ lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
}
- for (HbmEvent event : mEvents) {
- if (lastStartTime > event.endTimeMillis) {
+ for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
+ if (lastStartTime > event.getEndTimeMillis()) {
pw.println(" event: [normal brightness]: "
- + TimeUtils.formatDuration(lastStartTime - event.endTimeMillis));
+ + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
}
lastStartTime = dumpHbmEvent(pw, event);
}
@@ -338,12 +342,12 @@
}
private long dumpHbmEvent(PrintWriter pw, HbmEvent event) {
- final long duration = event.endTimeMillis - event.startTimeMillis;
+ final long duration = event.getEndTimeMillis() - event.getStartTimeMillis();
pw.println(" event: ["
- + TimeUtils.formatUptime(event.startTimeMillis) + ", "
- + TimeUtils.formatUptime(event.endTimeMillis) + "] ("
+ + TimeUtils.formatUptime(event.getStartTimeMillis()) + ", "
+ + TimeUtils.formatUptime(event.getEndTimeMillis()) + "] ("
+ TimeUtils.formatDuration(duration) + ")");
- return event.startTimeMillis;
+ return event.getStartTimeMillis();
}
private boolean isCurrentlyAllowed() {
@@ -372,13 +376,15 @@
// First, lets see how much time we've taken for any currently running
// session of HBM.
- if (mRunningStartTimeMillis > 0) {
- if (mRunningStartTimeMillis > currentTime) {
+ long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+ if (runningStartTimeMillis > 0) {
+ if (runningStartTimeMillis > currentTime) {
Slog.e(TAG, "Start time set to the future. curr: " + currentTime
- + ", start: " + mRunningStartTimeMillis);
- mRunningStartTimeMillis = currentTime;
+ + ", start: " + runningStartTimeMillis);
+ mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime);
+ runningStartTimeMillis = currentTime;
}
- timeAlreadyUsed = currentTime - mRunningStartTimeMillis;
+ timeAlreadyUsed = currentTime - runningStartTimeMillis;
}
if (DEBUG) {
@@ -387,18 +393,19 @@
// Next, lets iterate through the history of previous sessions and add those times.
final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis;
- Iterator<HbmEvent> it = mEvents.iterator();
+ Iterator<HbmEvent> it = mHighBrightnessModeMetadata.getHbmEventQueue().iterator();
while (it.hasNext()) {
final HbmEvent event = it.next();
// If this event ended before the current Timing window, discard forever and ever.
- if (event.endTimeMillis < windowstartTimeMillis) {
+ if (event.getEndTimeMillis() < windowstartTimeMillis) {
it.remove();
continue;
}
- final long startTimeMillis = Math.max(event.startTimeMillis, windowstartTimeMillis);
- timeAlreadyUsed += event.endTimeMillis - startTimeMillis;
+ final long startTimeMillis = Math.max(event.getStartTimeMillis(),
+ windowstartTimeMillis);
+ timeAlreadyUsed += event.getEndTimeMillis() - startTimeMillis;
}
if (DEBUG) {
@@ -425,17 +432,18 @@
// Calculate the time at which we want to recalculate mIsTimeAvailable in case a lux or
// brightness change doesn't happen before then.
long nextTimeout = -1;
+ final ArrayDeque<HbmEvent> hbmEvents = mHighBrightnessModeMetadata.getHbmEventQueue();
if (mBrightness > mHbmData.transitionPoint) {
// if we're in high-lux now, timeout when we run out of allowed time.
nextTimeout = currentTime + remainingTime;
- } else if (!mIsTimeAvailable && mEvents.size() > 0) {
+ } else if (!mIsTimeAvailable && hbmEvents.size() > 0) {
// If we are not allowed...timeout when the oldest event moved outside of the timing
// window by at least minTime. Basically, we're calculating the soonest time we can
// get {@code timeMinMillis} back to us.
final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis;
- final HbmEvent lastEvent = mEvents.getLast();
+ final HbmEvent lastEvent = hbmEvents.peekLast();
final long startTimePlusMinMillis =
- Math.max(windowstartTimeMillis, lastEvent.startTimeMillis)
+ Math.max(windowstartTimeMillis, lastEvent.getStartTimeMillis())
+ mHbmData.timeMinMillis;
final long timeWhenMinIsGainedBack =
currentTime + (startTimePlusMinMillis - windowstartTimeMillis) - remainingTime;
@@ -459,9 +467,10 @@
+ ", mUnthrottledBrightness: " + mUnthrottledBrightness
+ ", mThrottlingReason: "
+ BrightnessInfo.briMaxReasonToString(mThrottlingReason)
- + ", RunningStartTimeMillis: " + mRunningStartTimeMillis
+ + ", RunningStartTimeMillis: "
+ + mHighBrightnessModeMetadata.getRunningStartTimeMillis()
+ ", nextTimeout: " + (nextTimeout != -1 ? (nextTimeout - currentTime) : -1)
- + ", events: " + mEvents);
+ + ", events: " + hbmEvents);
}
if (nextTimeout != -1) {
@@ -588,25 +597,6 @@
}
}
- /**
- * Represents an event in which High Brightness Mode was enabled.
- */
- private static class HbmEvent {
- public long startTimeMillis;
- public long endTimeMillis;
-
- HbmEvent(long startTimeMillis, long endTimeMillis) {
- this.startTimeMillis = startTimeMillis;
- this.endTimeMillis = endTimeMillis;
- }
-
- @Override
- public String toString() {
- return "[Event: {" + startTimeMillis + ", " + endTimeMillis + "}, total: "
- + ((endTimeMillis - startTimeMillis) / 1000) + "]";
- }
- }
-
@VisibleForTesting
class HdrListener extends SurfaceControlHdrLayerInfoListener {
@Override
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java
new file mode 100644
index 0000000..8aa3631
--- /dev/null
+++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.display;
+
+import java.util.ArrayDeque;
+
+
+/**
+ * Represents High Brightness Mode metadata associated
+ * with a specific display.
+ * Required for separately storing data like time information,
+ * and related events when display was in HBM mode per display.
+ */
+class HighBrightnessModeMetadata {
+ /**
+ * Queue of previous HBM-events ordered from most recent to least recent.
+ * Meant to store only the events that fall into the most recent
+ * {@link HighBrightnessModeData#timeWindowMillis mHbmData.timeWindowMillis}.
+ */
+ private final ArrayDeque<HbmEvent> mEvents = new ArrayDeque<>();
+
+ /**
+ * If HBM is currently running, this is the start time for the current HBM session.
+ */
+ private long mRunningStartTimeMillis = -1;
+
+ public long getRunningStartTimeMillis() {
+ return mRunningStartTimeMillis;
+ }
+
+ public void setRunningStartTimeMillis(long setTime) {
+ mRunningStartTimeMillis = setTime;
+ }
+
+ public ArrayDeque<HbmEvent> getHbmEventQueue() {
+ return mEvents;
+ }
+
+ public void addHbmEvent(HbmEvent hbmEvent) {
+ mEvents.addFirst(hbmEvent);
+ }
+}
+
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index bf576b8..1f7232a 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -159,6 +159,7 @@
private Layout mCurrentLayout = null;
private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
private int mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
+ private int mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE;
private boolean mBootCompleted = false;
private boolean mInteractive;
@@ -353,6 +354,12 @@
ipw.println("mDeviceStatesOnWhichToWakeUp=" + mDeviceStatesOnWhichToWakeUp);
ipw.println("mDeviceStatesOnWhichToSleep=" + mDeviceStatesOnWhichToSleep);
ipw.println("mInteractive=" + mInteractive);
+ ipw.println("mBootCompleted=" + mBootCompleted);
+
+ ipw.println();
+ ipw.println("mDeviceState=" + mDeviceState);
+ ipw.println("mPendingDeviceState=" + mPendingDeviceState);
+ ipw.println("mDeviceStateToBeAppliedAfterBoot=" + mDeviceStateToBeAppliedAfterBoot);
final int logicalDisplayCount = mLogicalDisplays.size();
ipw.println();
@@ -370,6 +377,17 @@
}
void setDeviceStateLocked(int state, boolean isOverrideActive) {
+ if (!mBootCompleted) {
+ // The boot animation might still be in progress, we do not want to switch states now
+ // as the boot animation would end up with an incorrect size.
+ if (DEBUG) {
+ Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState
+ + " until boot is completed");
+ }
+ mDeviceStateToBeAppliedAfterBoot = state;
+ return;
+ }
+
Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
+ ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted);
// As part of a state transition, we may need to turn off some displays temporarily so that
@@ -378,6 +396,7 @@
// temporarily turned off.
resetLayoutLocked(mDeviceState, state, /* isStateChangeStarting= */ true);
mPendingDeviceState = state;
+ mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE;
final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState,
mInteractive, mBootCompleted);
final boolean sleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, mDeviceState,
@@ -424,6 +443,10 @@
void onBootCompleted() {
synchronized (mSyncRoot) {
mBootCompleted = true;
+ if (mDeviceStateToBeAppliedAfterBoot != DeviceStateManager.INVALID_DEVICE_STATE) {
+ setDeviceStateLocked(mDeviceStateToBeAppliedAfterBoot,
+ /* isOverrideActive= */ false);
+ }
}
}
@@ -477,7 +500,8 @@
@VisibleForTesting
boolean shouldDeviceBePutToSleep(int pendingState, int currentState, boolean isOverrideActive,
boolean isInteractive, boolean isBootCompleted) {
- return mDeviceStatesOnWhichToSleep.get(pendingState)
+ return currentState != DeviceStateManager.INVALID_DEVICE_STATE
+ && mDeviceStatesOnWhichToSleep.get(pendingState)
&& !mDeviceStatesOnWhichToSleep.get(currentState)
&& !isOverrideActive
&& isInteractive && isBootCompleted;
@@ -926,6 +950,15 @@
final int layerStack = assignLayerStackLocked(displayId);
final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device);
display.updateLocked(mDisplayDeviceRepo);
+
+ final DisplayInfo info = display.getDisplayInfoLocked();
+ if (info.type == Display.TYPE_INTERNAL && mDeviceStateToLayoutMap.size() > 1) {
+ // If this is an internal display and the device uses a display layout configuration,
+ // the display should be disabled as later we will receive a device state update, which
+ // will tell us which internal displays should be enabled and which should be disabled.
+ display.setEnabledLocked(false);
+ }
+
mLogicalDisplays.put(displayId, display);
return display;
}
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 017b96c..2276715 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -107,11 +107,6 @@
Matrix.setIdentityM(MATRIX_IDENTITY, 0);
}
- /**
- * The transition time, in milliseconds, for Night Display to turn on/off.
- */
- private static final long TRANSITION_DURATION = 3000L;
-
private static final int MSG_USER_CHANGED = 0;
private static final int MSG_SET_UP = 1;
private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2;
@@ -661,7 +656,7 @@
TintValueAnimator valueAnimator = TintValueAnimator.ofMatrix(COLOR_MATRIX_EVALUATOR,
from == null ? MATRIX_IDENTITY : from, to);
tintController.setAnimator(valueAnimator);
- valueAnimator.setDuration(TRANSITION_DURATION);
+ valueAnimator.setDuration(tintController.getTransitionDurationMilliseconds());
valueAnimator.setInterpolator(AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.fast_out_slow_in));
valueAnimator.addUpdateListener((ValueAnimator animator) -> {
diff --git a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
index 93a78c1..f27ccc7 100644
--- a/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
+++ b/services/core/java/com/android/server/display/color/DisplayWhiteBalanceTintController.java
@@ -59,7 +59,8 @@
private float[] mCurrentColorTemperatureXYZ;
@VisibleForTesting
boolean mSetUp = false;
- private float[] mMatrixDisplayWhiteBalance = new float[16];
+ private final float[] mMatrixDisplayWhiteBalance = new float[16];
+ private long mTransitionDuration;
private Boolean mIsAvailable;
// This feature becomes disallowed if the device is in an unsupported strong/light state.
private boolean mIsAllowed = true;
@@ -119,6 +120,9 @@
final int colorTemperature = res.getInteger(
R.integer.config_displayWhiteBalanceColorTemperatureDefault);
+ mTransitionDuration = res.getInteger(
+ R.integer.config_displayWhiteBalanceTransitionTime);
+
synchronized (mLock) {
mDisplayColorSpaceRGB = displayColorSpaceRGB;
mDisplayNominalWhiteXYZ = displayNominalWhiteXYZ;
@@ -232,6 +236,11 @@
}
@Override
+ public long getTransitionDurationMilliseconds() {
+ return mTransitionDuration;
+ }
+
+ @Override
public void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println(" mSetUp = " + mSetUp);
diff --git a/services/core/java/com/android/server/display/color/TintController.java b/services/core/java/com/android/server/display/color/TintController.java
index 422dd32..c53ac06 100644
--- a/services/core/java/com/android/server/display/color/TintController.java
+++ b/services/core/java/com/android/server/display/color/TintController.java
@@ -16,7 +16,6 @@
package com.android.server.display.color;
-import android.animation.ValueAnimator;
import android.content.Context;
import android.util.Slog;
@@ -24,6 +23,11 @@
abstract class TintController {
+ /**
+ * The default transition time, in milliseconds, for color transforms to turn on/off.
+ */
+ private static final long TRANSITION_DURATION = 3000L;
+
private ColorDisplayService.TintValueAnimator mAnimator;
private Boolean mIsActivated;
@@ -66,6 +70,10 @@
return mIsActivated == null;
}
+ public long getTransitionDurationMilliseconds() {
+ return TRANSITION_DURATION;
+ }
+
/**
* Dump debug information.
*/
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index db9deb1..f87a146 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -244,7 +244,10 @@
}
mListener.onDreamStopped(dream.mToken);
+ } else if (dream.mCanDoze && !mCurrentDream.mCanDoze) {
+ mListener.stopDozing(dream.mToken);
}
+
} finally {
Trace.traceEnd(Trace.TRACE_TAG_POWER);
}
@@ -289,6 +292,7 @@
*/
public interface Listener {
void onDreamStopped(Binder token);
+ void stopDozing(Binder token);
}
private final class DreamRecord implements DeathRecipient, ServiceConnection {
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index bb1e393..148b80e 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -499,7 +499,12 @@
}
synchronized (mLock) {
- if (mCurrentDream != null && mCurrentDream.token == token && mCurrentDream.isDozing) {
+ if (mCurrentDream == null) {
+ return;
+ }
+
+ final boolean sameDream = mCurrentDream.token == token;
+ if ((sameDream && mCurrentDream.isDozing) || (!sameDream && !mCurrentDream.isDozing)) {
mCurrentDream.isDozing = false;
mDozeWakeLock.release();
mPowerManagerInternal.setDozeOverrideFromDreamManager(
@@ -765,6 +770,11 @@
}
}
}
+
+ @Override
+ public void stopDozing(Binder token) {
+ stopDozingInternal(token);
+ }
};
private final ContentObserver mDozeEnabledObserver = new ContentObserver(null) {
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 4d55d4e..25fefad 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -132,6 +132,7 @@
// contains connections to all connected services, including app services
// and system services
+ @GuardedBy("mMutex")
private final ArrayList<ManagedServiceInfo> mServices = new ArrayList<>();
/**
* The services that have been bound by us. If the service is also connected, it will also
@@ -150,13 +151,15 @@
= new ArraySet<>();
// Just the packages from mEnabledServicesForCurrentProfiles
private ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
- // List of enabled packages that have nevertheless asked not to be run
- private ArraySet<ComponentName> mSnoozingForCurrentProfiles = new ArraySet<>();
+ // Per user id, list of enabled packages that have nevertheless asked not to be run
+ private final android.util.SparseSetArray<ComponentName> mSnoozing =
+ new android.util.SparseSetArray<>();
// List of approved packages or components (by user, then by primary/secondary) that are
// allowed to be bound as managed services. A package or component appearing in this list does
// not mean that we are currently bound to said package/component.
- protected ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved = new ArrayMap<>();
+ protected final ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved =
+ new ArrayMap<>();
// List of packages or components (by user) that are configured to be enabled/disabled
// explicitly by the user
@@ -315,6 +318,7 @@
return changes;
}
+ @GuardedBy("mApproved")
private boolean clearUserSetFlagLocked(ComponentName component, int userId) {
String approvedValue = getApprovedValue(component.flattenToString());
ArraySet<String> userSet = mUserSetServices.get(userId);
@@ -375,8 +379,8 @@
pw.println(" " + cmpt);
}
- pw.println(" Live " + getCaption() + "s (" + mServices.size() + "):");
synchronized (mMutex) {
+ pw.println(" Live " + getCaption() + "s (" + mServices.size() + "):");
for (ManagedServiceInfo info : mServices) {
if (filter != null && !filter.matches(info.component)) continue;
pw.println(" " + info.component
@@ -386,10 +390,15 @@
}
}
- pw.println(" Snoozed " + getCaption() + "s (" +
- mSnoozingForCurrentProfiles.size() + "):");
- for (ComponentName name : mSnoozingForCurrentProfiles) {
- pw.println(" " + name.flattenToShortString());
+ synchronized (mSnoozing) {
+ pw.println(" Snoozed " + getCaption() + "s ("
+ + mSnoozing.size() + "):");
+ for (int i = 0; i < mSnoozing.size(); i++) {
+ pw.println(" User: " + mSnoozing.keyAt(i));
+ for (ComponentName name : mSnoozing.valuesAt(i)) {
+ pw.println(" " + name.flattenToShortString());
+ }
+ }
}
}
@@ -431,8 +440,16 @@
}
}
- for (ComponentName name : mSnoozingForCurrentProfiles) {
- name.dumpDebug(proto, ManagedServicesProto.SNOOZED);
+ synchronized (mSnoozing) {
+ for (int i = 0; i < mSnoozing.size(); i++) {
+ long token = proto.start(ManagedServicesProto.SNOOZED);
+ proto.write(ManagedServicesProto.SnoozedServices.USER_ID,
+ mSnoozing.keyAt(i));
+ for (ComponentName name : mSnoozing.valuesAt(i)) {
+ name.dumpDebug(proto, ManagedServicesProto.SnoozedServices.SNOOZED);
+ }
+ proto.end(token);
+ }
}
}
@@ -975,6 +992,9 @@
synchronized (mApproved) {
mApproved.remove(user);
}
+ synchronized (mSnoozing) {
+ mSnoozing.remove(user);
+ }
rebindServices(true, user);
}
@@ -994,10 +1014,12 @@
return null;
}
final IBinder token = service.asBinder();
- final int N = mServices.size();
- for (int i = 0; i < N; i++) {
- final ManagedServiceInfo info = mServices.get(i);
- if (info.service.asBinder() == token) return info;
+ synchronized (mMutex) {
+ final int nServices = mServices.size();
+ for (int i = 0; i < nServices; i++) {
+ final ManagedServiceInfo info = mServices.get(i);
+ if (info.service.asBinder() == token) return info;
+ }
}
return null;
}
@@ -1066,15 +1088,17 @@
}
protected void setComponentState(ComponentName component, int userId, boolean enabled) {
- boolean previous = !mSnoozingForCurrentProfiles.contains(component);
- if (previous == enabled) {
- return;
- }
+ synchronized (mSnoozing) {
+ boolean previous = !mSnoozing.contains(userId, component);
+ if (previous == enabled) {
+ return;
+ }
- if (enabled) {
- mSnoozingForCurrentProfiles.remove(component);
- } else {
- mSnoozingForCurrentProfiles.add(component);
+ if (enabled) {
+ mSnoozing.remove(userId, component);
+ } else {
+ mSnoozing.add(userId, component);
+ }
}
// State changed
@@ -1287,7 +1311,10 @@
}
final Set<ComponentName> add = new HashSet<>(userComponents);
- add.removeAll(mSnoozingForCurrentProfiles);
+ ArraySet<ComponentName> snoozed = mSnoozing.get(userId);
+ if (snoozed != null) {
+ add.removeAll(snoozed);
+ }
componentsToBind.put(userId, add);
@@ -1466,10 +1493,12 @@
}
}
+ @GuardedBy("mMutex")
private void registerServiceLocked(final ComponentName name, final int userid) {
registerServiceLocked(name, userid, false /* isSystem */);
}
+ @GuardedBy("mMutex")
private void registerServiceLocked(final ComponentName name, final int userid,
final boolean isSystem) {
if (DEBUG) Slog.v(TAG, "registerService: " + name + " u=" + userid);
@@ -1600,6 +1629,7 @@
}
}
+ @GuardedBy("mMutex")
private void unregisterServiceLocked(ComponentName name, int userid) {
final int N = mServices.size();
for (int i = N - 1; i >= 0; i--) {
@@ -1634,6 +1664,7 @@
return serviceInfo;
}
+ @GuardedBy("mMutex")
private ManagedServiceInfo removeServiceLocked(int i) {
final ManagedServiceInfo info = mServices.remove(i);
onServiceRemovedLocked(info);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 195101d..f2f312a 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -239,7 +239,6 @@
import android.service.notification.NotificationRecordProto;
import android.service.notification.NotificationServiceDumpProto;
import android.service.notification.NotificationStats;
-import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeProto;
@@ -8568,95 +8567,6 @@
}
}
- static class NotificationRecordExtractorData {
- // Class that stores any field in a NotificationRecord that can change via an extractor.
- // Used to cache previous data used in a sort.
- int mPosition;
- int mVisibility;
- boolean mShowBadge;
- boolean mAllowBubble;
- boolean mIsBubble;
- NotificationChannel mChannel;
- String mGroupKey;
- ArrayList<String> mOverridePeople;
- ArrayList<SnoozeCriterion> mSnoozeCriteria;
- Integer mUserSentiment;
- Integer mSuppressVisually;
- ArrayList<Notification.Action> mSystemSmartActions;
- ArrayList<CharSequence> mSmartReplies;
- int mImportance;
-
- // These fields may not trigger a reranking but diffs here may be logged.
- float mRankingScore;
- boolean mIsConversation;
-
- NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
- boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
- ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
- Integer userSentiment, Integer suppressVisually,
- ArrayList<Notification.Action> systemSmartActions,
- ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
- boolean isConversation) {
- mPosition = position;
- mVisibility = visibility;
- mShowBadge = showBadge;
- mAllowBubble = allowBubble;
- mIsBubble = isBubble;
- mChannel = channel;
- mGroupKey = groupKey;
- mOverridePeople = overridePeople;
- mSnoozeCriteria = snoozeCriteria;
- mUserSentiment = userSentiment;
- mSuppressVisually = suppressVisually;
- mSystemSmartActions = systemSmartActions;
- mSmartReplies = smartReplies;
- mImportance = importance;
- mRankingScore = rankingScore;
- mIsConversation = isConversation;
- }
-
- // Returns whether the provided NotificationRecord differs from the cached data in any way.
- // Should be guarded by mNotificationLock; not annotated here as this class is static.
- boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
- return mPosition != newPosition
- || mVisibility != r.getPackageVisibilityOverride()
- || mShowBadge != r.canShowBadge()
- || mAllowBubble != r.canBubble()
- || mIsBubble != r.getNotification().isBubbleNotification()
- || !Objects.equals(mChannel, r.getChannel())
- || !Objects.equals(mGroupKey, r.getGroupKey())
- || !Objects.equals(mOverridePeople, r.getPeopleOverride())
- || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
- || !Objects.equals(mUserSentiment, r.getUserSentiment())
- || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
- || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
- || !Objects.equals(mSmartReplies, r.getSmartReplies())
- || mImportance != r.getImportance();
- }
-
- // Returns whether the NotificationRecord has a change from this data for which we should
- // log an update. This method specifically targets fields that may be changed via
- // adjustments from the assistant.
- //
- // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
- // and NotificationRecord.applyAdjustments.
- //
- // Should be guarded by mNotificationLock; not annotated here as this class is static.
- boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
- return mPosition != newPosition
- || !Objects.equals(mChannel, r.getChannel())
- || !Objects.equals(mGroupKey, r.getGroupKey())
- || !Objects.equals(mOverridePeople, r.getPeopleOverride())
- || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
- || !Objects.equals(mUserSentiment, r.getUserSentiment())
- || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
- || !Objects.equals(mSmartReplies, r.getSmartReplies())
- || mImportance != r.getImportance()
- || !r.rankingScoreMatches(mRankingScore)
- || mIsConversation != r.isConversation();
- }
- }
-
void handleRankingSort() {
if (mRankingHelper == null) return;
synchronized (mNotificationLock) {
@@ -8682,7 +8592,8 @@
r.getSmartReplies(),
r.getImportance(),
r.getRankingScore(),
- r.isConversation());
+ r.isConversation(),
+ r.getProposedImportance());
extractorDataBefore.put(r.getKey(), extractorData);
mRankingHelper.extractSignals(r);
}
@@ -9977,7 +9888,8 @@
record.getRankingScore() == 0
? RANKING_UNCHANGED
: (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED),
- record.getNotification().isBubbleNotification()
+ record.getNotification().isBubbleNotification(),
+ record.getProposedImportance()
);
rankings.add(ranking);
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index cbaf485..d344306 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -210,6 +210,7 @@
// Whether this notification record should have an update logged the next time notifications
// are sorted.
private boolean mPendingLogUpdate = false;
+ private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
public NotificationRecord(Context context, StatusBarNotification sbn,
NotificationChannel channel) {
@@ -499,6 +500,8 @@
pw.println(prefix + "mImportance="
+ NotificationListenerService.Ranking.importanceToString(mImportance));
pw.println(prefix + "mImportanceExplanation=" + getImportanceExplanation());
+ pw.println(prefix + "mProposedImportance="
+ + NotificationListenerService.Ranking.importanceToString(mProposedImportance));
pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
pw.println(prefix + "mIntercept=" + mIntercept);
pw.println(prefix + "mHidden==" + mHidden);
@@ -738,6 +741,12 @@
Adjustment.KEY_NOT_CONVERSATION,
Boolean.toString(mIsNotConversationOverride));
}
+ if (signals.containsKey(Adjustment.KEY_IMPORTANCE_PROPOSAL)) {
+ mProposedImportance = signals.getInt(Adjustment.KEY_IMPORTANCE_PROPOSAL);
+ EventLogTags.writeNotificationAdjusted(getKey(),
+ Adjustment.KEY_IMPORTANCE_PROPOSAL,
+ Integer.toString(mProposedImportance));
+ }
if (!signals.isEmpty() && adjustment.getIssuer() != null) {
mAdjustmentIssuer = adjustment.getIssuer();
}
@@ -870,6 +879,10 @@
return stats.naturalImportance;
}
+ public int getProposedImportance() {
+ return mProposedImportance;
+ }
+
public float getRankingScore() {
return mRankingScore;
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
new file mode 100644
index 0000000..6dc9029
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
@@ -0,0 +1,118 @@
+/*
+ * 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.server.notification;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.service.notification.SnoozeCriterion;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Class that stores any field in a NotificationRecord that can change via an extractor.
+ * Used to cache previous data used in a sort.
+ */
+public final class NotificationRecordExtractorData {
+ private final int mPosition;
+ private final int mVisibility;
+ private final boolean mShowBadge;
+ private final boolean mAllowBubble;
+ private final boolean mIsBubble;
+ private final NotificationChannel mChannel;
+ private final String mGroupKey;
+ private final ArrayList<String> mOverridePeople;
+ private final ArrayList<SnoozeCriterion> mSnoozeCriteria;
+ private final Integer mUserSentiment;
+ private final Integer mSuppressVisually;
+ private final ArrayList<Notification.Action> mSystemSmartActions;
+ private final ArrayList<CharSequence> mSmartReplies;
+ private final int mImportance;
+
+ // These fields may not trigger a reranking but diffs here may be logged.
+ private final float mRankingScore;
+ private final boolean mIsConversation;
+ private final int mProposedImportance;
+
+ NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
+ boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
+ ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
+ Integer userSentiment, Integer suppressVisually,
+ ArrayList<Notification.Action> systemSmartActions,
+ ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
+ boolean isConversation, int proposedImportance) {
+ mPosition = position;
+ mVisibility = visibility;
+ mShowBadge = showBadge;
+ mAllowBubble = allowBubble;
+ mIsBubble = isBubble;
+ mChannel = channel;
+ mGroupKey = groupKey;
+ mOverridePeople = overridePeople;
+ mSnoozeCriteria = snoozeCriteria;
+ mUserSentiment = userSentiment;
+ mSuppressVisually = suppressVisually;
+ mSystemSmartActions = systemSmartActions;
+ mSmartReplies = smartReplies;
+ mImportance = importance;
+ mRankingScore = rankingScore;
+ mIsConversation = isConversation;
+ mProposedImportance = proposedImportance;
+ }
+
+ // Returns whether the provided NotificationRecord differs from the cached data in any way.
+ // Should be guarded by mNotificationLock; not annotated here as this class is static.
+ boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
+ return mPosition != newPosition
+ || mVisibility != r.getPackageVisibilityOverride()
+ || mShowBadge != r.canShowBadge()
+ || mAllowBubble != r.canBubble()
+ || mIsBubble != r.getNotification().isBubbleNotification()
+ || !Objects.equals(mChannel, r.getChannel())
+ || !Objects.equals(mGroupKey, r.getGroupKey())
+ || !Objects.equals(mOverridePeople, r.getPeopleOverride())
+ || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
+ || !Objects.equals(mUserSentiment, r.getUserSentiment())
+ || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
+ || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
+ || !Objects.equals(mSmartReplies, r.getSmartReplies())
+ || mImportance != r.getImportance()
+ || mProposedImportance != r.getProposedImportance();
+ }
+
+ // Returns whether the NotificationRecord has a change from this data for which we should
+ // log an update. This method specifically targets fields that may be changed via
+ // adjustments from the assistant.
+ //
+ // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
+ // and NotificationRecord.applyAdjustments.
+ //
+ // Should be guarded by mNotificationLock; not annotated here as this class is static.
+ boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
+ return mPosition != newPosition
+ || !Objects.equals(mChannel, r.getChannel())
+ || !Objects.equals(mGroupKey, r.getGroupKey())
+ || !Objects.equals(mOverridePeople, r.getPeopleOverride())
+ || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
+ || !Objects.equals(mUserSentiment, r.getUserSentiment())
+ || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
+ || !Objects.equals(mSmartReplies, r.getSmartReplies())
+ || mImportance != r.getImportance()
+ || !r.rankingScoreMatches(mRankingScore)
+ || mIsConversation != r.isConversation()
+ || mProposedImportance != r.getProposedImportance();
+ }
+}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 78dad12..7ca9ac7 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -108,7 +108,7 @@
@VisibleForTesting
static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000;
@VisibleForTesting
- static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 50000;
+ static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000;
private static final int NOTIFICATION_PREFERENCES_PULL_LIMIT = 1000;
private static final int NOTIFICATION_CHANNEL_PULL_LIMIT = 2000;
@@ -1007,6 +1007,7 @@
channel.setAllowBubbles(existing != null
? existing.getAllowBubbles()
: NotificationChannel.DEFAULT_ALLOW_BUBBLE);
+ channel.setImportantConversation(false);
}
clearLockedFieldsLocked(channel);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 6d63891..89a920a 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -91,6 +91,7 @@
import android.service.gatekeeper.IGateKeeperService;
import android.service.voice.VoiceInteractionManagerInternal;
import android.stats.devicepolicy.DevicePolicyEnums;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -1763,6 +1764,63 @@
}
}
+ /**
+ * Returns whether switching users is currently allowed for the provided user.
+ * <p>
+ * Switching users is not allowed in the following cases:
+ * <li>the user is in a phone call</li>
+ * <li>{@link UserManager#DISALLOW_USER_SWITCH} is set</li>
+ * <li>system user hasn't been unlocked yet</li>
+ *
+ * @return A {@link UserManager.UserSwitchabilityResult} flag indicating if the user is
+ * switchable.
+ */
+ public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) {
+ checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserSwitchability");
+
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ t.traceBegin("getUserSwitchability-" + userId);
+
+ int flags = UserManager.SWITCHABILITY_STATUS_OK;
+
+ t.traceBegin("TM.isInCall");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+ if (telecomManager != null && telecomManager.isInCall()) {
+ flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ t.traceEnd();
+
+ t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
+ if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
+ flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
+ }
+ t.traceEnd();
+
+ // System User is always unlocked in Headless System User Mode, so ignore this flag
+ if (!UserManager.isHeadlessSystemUserMode()) {
+ t.traceBegin("getInt-ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED");
+ final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
+ mContext.getContentResolver(),
+ Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
+ t.traceEnd();
+ t.traceBegin("isUserUnlocked-USER_SYSTEM");
+ final boolean systemUserUnlocked = mLocalService.isUserUnlocked(UserHandle.USER_SYSTEM);
+ t.traceEnd();
+
+ if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
+ flags |= UserManager.SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
+ }
+ }
+ t.traceEnd();
+
+ return flags;
+ }
+
@Override
public boolean isUserSwitcherEnabled(@UserIdInt int mUserId) {
boolean multiUserSettingOn = Settings.Global.getInt(mContext.getContentResolver(),
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 554e269..20c9a21 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -4215,7 +4215,6 @@
}
boolean changed = false;
- Set<Permission> needsUpdate = null;
synchronized (mLock) {
final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator();
while (it.hasNext()) {
@@ -4234,26 +4233,6 @@
+ " that used to be declared by " + bp.getPackageName());
it.remove();
}
- if (needsUpdate == null) {
- needsUpdate = new ArraySet<>();
- }
- needsUpdate.add(bp);
- }
- }
- if (needsUpdate != null) {
- for (final Permission bp : needsUpdate) {
- final AndroidPackage sourcePkg =
- mPackageManagerInt.getPackage(bp.getPackageName());
- final PackageStateInternal sourcePs =
- mPackageManagerInt.getPackageStateInternal(bp.getPackageName());
- synchronized (mLock) {
- if (sourcePkg != null && sourcePs != null) {
- continue;
- }
- Slog.w(TAG, "Removing dangling permission tree: " + bp.getName()
- + " from package " + bp.getPackageName());
- mRegistry.removePermission(bp.getName());
- }
}
}
return changed;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index c8a11a5..53cfede 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -568,6 +568,9 @@
// What we do when the user double-taps on home
private int mDoubleTapOnHomeBehavior;
+ // Whether to lock the device after the next app transition has finished.
+ private boolean mLockAfterAppTransitionFinished;
+
// Allowed theater mode wake actions
private boolean mAllowTheaterModeWakeFromKey;
private boolean mAllowTheaterModeWakeFromPowerKey;
@@ -711,7 +714,7 @@
handleRingerChordGesture();
break;
case MSG_SCREENSHOT_CHORD:
- handleScreenShot(msg.arg1, msg.arg2);
+ handleScreenShot(msg.arg1);
break;
}
}
@@ -974,12 +977,7 @@
powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
} else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
Slog.d(TAG, "No behavior defined for power press count " + count);
- } else if (count == 1 && interactive) {
- if (beganFromNonInteractive) {
- // The screen off case, where we might want to start dreaming on power button press.
- attemptToDreamFromShortPowerButtonPress(false, () -> {});
- return;
- }
+ } else if (count == 1 && interactive && !beganFromNonInteractive) {
if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) {
Slog.i(TAG, "Suppressing power key because the user is interacting with the "
+ "fingerprint sensor");
@@ -1057,11 +1055,10 @@
return;
}
- // Make sure the device locks. Unfortunately, this has the side-effect of briefly revealing
- // the lock screen before the dream appears. Note that locking is a side-effect of the no
- // dream action that is executed if we early return above.
- // TODO(b/261662912): Find a better way to lock the device that doesn't result in jank.
- lockNow(null);
+ synchronized (mLock) {
+ // Lock the device after the dream transition has finished.
+ mLockAfterAppTransitionFinished = true;
+ }
dreamManagerInternal.requestDream();
}
@@ -1502,9 +1499,9 @@
|| mShortPressOnStemPrimaryBehavior != SHORT_PRESS_PRIMARY_NOTHING;
}
- private void interceptScreenshotChord(int type, int source, long pressDelay) {
+ private void interceptScreenshotChord(int source, long pressDelay) {
mHandler.removeMessages(MSG_SCREENSHOT_CHORD);
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCREENSHOT_CHORD, type, source),
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SCREENSHOT_CHORD, source),
pressDelay);
}
@@ -1574,9 +1571,8 @@
}
};
- private void handleScreenShot(@WindowManager.ScreenshotType int type,
- @WindowManager.ScreenshotSource int source) {
- mDefaultDisplayPolicy.takeScreenshot(type, source);
+ private void handleScreenShot(@WindowManager.ScreenshotSource int source) {
+ mDefaultDisplayPolicy.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, source);
}
@Override
@@ -2142,6 +2138,22 @@
handleStartTransitionForKeyguardLw(
keyguardGoingAway, false /* keyguardOccludingStarted */,
0 /* duration */);
+
+ synchronized (mLock) {
+ mLockAfterAppTransitionFinished = false;
+ }
+ }
+
+ @Override
+ public void onAppTransitionFinishedLocked(IBinder token) {
+ synchronized (mLock) {
+ if (!mLockAfterAppTransitionFinished) {
+ return;
+ }
+ mLockAfterAppTransitionFinished = false;
+ }
+
+ lockNow(null);
}
});
@@ -2173,7 +2185,7 @@
@Override
void execute() {
mPowerKeyHandled = true;
- interceptScreenshotChord(TAKE_SCREENSHOT_FULLSCREEN,
+ interceptScreenshotChord(
SCREENSHOT_KEY_CHORD, getScreenshotChordLongPressDelay());
}
@Override
@@ -2867,8 +2879,7 @@
break;
case KeyEvent.KEYCODE_S:
if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) {
- interceptScreenshotChord(
- TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+ interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
return key_consumed;
}
break;
@@ -3252,8 +3263,7 @@
break;
case KeyEvent.KEYCODE_SYSRQ:
if (down && repeatCount == 0) {
- interceptScreenshotChord(
- TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+ interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
}
return true;
}
@@ -4152,9 +4162,6 @@
case KeyEvent.KEYCODE_DEMO_APP_2:
case KeyEvent.KEYCODE_DEMO_APP_3:
case KeyEvent.KEYCODE_DEMO_APP_4: {
- // TODO(b/254604589): Dispatch KeyEvent to System UI.
- sendSystemKeyToStatusBarAsync(keyCode);
-
// Just drop if keys are not intercepted for direct key.
result &= ~ACTION_PASS_TO_USER;
break;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index e7221c8..fde0b34 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -191,4 +191,12 @@
* @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp
*/
void enterStageSplitFromRunningApp(boolean leftOrTop);
+
+ /**
+ * Shows the media output switcher dialog.
+ *
+ * @param packageName of the session for which the output switcher is shown.
+ * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher
+ */
+ void showMediaOutputSwitcher(String packageName);
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 45748e6..ab7292d 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -714,6 +714,16 @@
} catch (RemoteException ex) { }
}
}
+
+ @Override
+ public void showMediaOutputSwitcher(String packageName) {
+ if (mBar != null) {
+ try {
+ mBar.showMediaOutputSwitcher(packageName);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
};
private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 7252545..7489f80 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -709,7 +709,8 @@
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
return r != null
- ? r.getRequestedOrientation() : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ ? r.getOverrideOrientation()
+ : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 6b01a77..2ebf8d9 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -439,6 +439,9 @@
// finished destroying itself.
private static final int DESTROY_TIMEOUT = 10 * 1000;
+ // Rounding tolerance to be used in aspect ratio computations
+ private static final float ASPECT_RATIO_ROUNDING_TOLERANCE = 0.005f;
+
final ActivityTaskManagerService mAtmService;
@NonNull
final ActivityInfo info; // activity info provided by developer in AndroidManifest
@@ -1165,8 +1168,10 @@
pw.println(prefix + "mVoiceInteraction=true");
}
pw.print(prefix); pw.print("mOccludesParent="); pw.println(mOccludesParent);
- pw.print(prefix); pw.print("mOrientation=");
- pw.println(ActivityInfo.screenOrientationToString(mOrientation));
+ pw.print(prefix); pw.print("overrideOrientation=");
+ pw.println(ActivityInfo.screenOrientationToString(getOverrideOrientation()));
+ pw.print(prefix); pw.print("requestedOrientation=");
+ pw.println(ActivityInfo.screenOrientationToString(super.getOverrideOrientation()));
pw.println(prefix + "mVisibleRequested=" + mVisibleRequested
+ " mVisible=" + mVisible + " mClientVisible=" + isClientVisible()
+ ((mDeferHidingClient) ? " mDeferHidingClient=" + mDeferHidingClient : "")
@@ -1964,6 +1969,15 @@
new ComponentName(info.packageName, info.targetActivity);
}
+ // Don't move below setActivityType since it triggers onConfigurationChange ->
+ // resolveOverrideConfiguration that requires having mLetterboxUiController initialised.
+ // Don't move below setOrientation(info.screenOrientation) since it triggers
+ // getOverrideOrientation that requires having mLetterboxUiController
+ // initialised.
+ mLetterboxUiController = new LetterboxUiController(mWmService, this);
+ mCameraCompatControlEnabled = mWmService.mContext.getResources()
+ .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
+
mTargetSdk = info.applicationInfo.targetSdkVersion;
mShowForAllUsers = (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0;
setOrientation(info.screenOrientation);
@@ -2084,12 +2098,6 @@
launchMode = aInfo.launchMode;
- // Don't move below setActivityType since it triggers onConfigurationChange ->
- // resolveOverrideConfiguration that requires having mLetterboxUiController initialised.
- mLetterboxUiController = new LetterboxUiController(mWmService, this);
- mCameraCompatControlEnabled = mWmService.mContext.getResources()
- .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
-
setActivityType(_componentSpecified, _launchedFromUid, _intent, options, sourceRecord);
immersive = (aInfo.flags & FLAG_IMMERSIVE) != 0;
@@ -2486,7 +2494,8 @@
if (topAttached != null) {
if (topAttached.isSnapshotCompatible(snapshot)
// This trampoline must be the same rotation.
- && mDisplayContent.getDisplayRotation().rotationForOrientation(mOrientation,
+ && mDisplayContent.getDisplayRotation().rotationForOrientation(
+ getOverrideOrientation(),
mDisplayContent.getRotation()) == snapshot.getRotation()) {
return STARTING_WINDOW_TYPE_SNAPSHOT;
}
@@ -7704,13 +7713,13 @@
return mLetterboxUiController.getInheritedOrientation();
}
}
- if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) {
+ if (task != null && getOverrideOrientation() == SCREEN_ORIENTATION_BEHIND) {
// We use Task here because we want to be consistent with what happens in
// multi-window mode where other tasks orientations are ignored.
final ActivityRecord belowCandidate = task.getActivity(
- a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing
- && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this,
- false /* includeBoundary */, true /* traverseTopToBottom */);
+ a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
+ this /* boundary */, false /* includeBoundary */,
+ true /* traverseTopToBottom */);
if (belowCandidate != null) {
return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
}
@@ -7718,6 +7727,19 @@
return super.getRequestedConfigurationOrientation(forDisplay);
}
+ /**
+ * Whether this activity can be used as an orientation source for activities above with
+ * {@link SCREEN_ORIENTATION_BEHIND}.
+ */
+ boolean canDefineOrientationForActivitiesAbove() {
+ if (finishing) {
+ return false;
+ }
+ final int overrideOrientation = getOverrideOrientation();
+ return overrideOrientation != SCREEN_ORIENTATION_UNSET
+ && overrideOrientation != SCREEN_ORIENTATION_BEHIND;
+ }
+
@Override
void onCancelFixedRotationTransform(int originalDisplayRotation) {
if (this != mDisplayContent.getLastOrientationSource()) {
@@ -7744,7 +7766,7 @@
}
}
- void setRequestedOrientation(int requestedOrientation) {
+ void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) {
if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) {
return;
}
@@ -7789,7 +7811,7 @@
// Allow app to specify orientation regardless of its visibility state if the current
// candidate want us to use orientation behind. I.e. the visible app on-top of this one
// wants us to use the orientation of the app behind it.
- return mOrientation;
+ return getOverrideOrientation();
}
// The {@link ActivityRecord} should only specify an orientation when it is not closing.
@@ -7797,15 +7819,31 @@
// task being started in the wrong orientation during the transition.
if (!getDisplayContent().mClosingApps.contains(this)
&& (isVisibleRequested() || getDisplayContent().mOpeningApps.contains(this))) {
- return mOrientation;
+ return getOverrideOrientation();
}
return SCREEN_ORIENTATION_UNSET;
}
- /** Returns the app's preferred orientation regardless of its currently visibility state. */
+ /**
+ * Returns the app's preferred orientation regardless of its current visibility state taking
+ * into account orientation per-app overrides applied by the device manufacturers.
+ */
+ @Override
+ protected int getOverrideOrientation() {
+ return mLetterboxUiController.overrideOrientationIfNeeded(super.getOverrideOrientation());
+ }
+
+ /**
+ * Returns the app's preferred orientation regardless of its currently visibility state. This
+ * is used to return a requested value to an app if they call {@link
+ * android.app.Activity#getRequestedOrientation} since {@link #getOverrideOrientation} value
+ * with override can confuse an app if it's different from what they requested with {@link
+ * android.app.Activity#setRequestedOrientation}.
+ */
+ @ActivityInfo.ScreenOrientation
int getRequestedOrientation() {
- return mOrientation;
+ return super.getOverrideOrientation();
}
/**
@@ -8204,8 +8242,12 @@
if (screenResolvedBounds.width() <= parentAppBounds.width()) {
float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
newParentConfiguration);
- offsetX = (int) Math.ceil((parentAppBounds.width() - screenResolvedBounds.width())
- * positionMultiplier);
+ offsetX = Math.max(0, (int) Math.ceil((parentAppBounds.width()
+ - screenResolvedBounds.width()) * positionMultiplier)
+ // This is added to make sure that insets added inside
+ // CompatDisplayInsets#getContainerBounds() do not break the alignment
+ // provided by the positionMultiplier
+ - screenResolvedBounds.left + parentAppBounds.left);
}
}
@@ -8215,8 +8257,12 @@
if (screenResolvedBounds.height() <= parentAppBounds.height()) {
float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier(
newParentConfiguration);
- offsetY = (int) Math.ceil((parentAppBounds.height() - screenResolvedBounds.height())
- * positionMultiplier);
+ offsetY = Math.max(0, (int) Math.ceil((parentAppBounds.height()
+ - screenResolvedBounds.height()) * positionMultiplier)
+ // This is added to make sure that insets added inside
+ // CompatDisplayInsets#getContainerBounds() do not break the alignment
+ // provided by the positionMultiplier
+ - screenResolvedBounds.top + parentAppBounds.top);
}
}
@@ -8357,8 +8403,8 @@
// If orientation is respected when insets are applied, then stableBounds will be empty.
boolean orientationRespectedWithInsets =
orientationRespectedWithInsets(parentBounds, stableBounds);
- if (orientationRespectedWithInsets
- && handlesOrientationChangeFromDescendant(mOrientation)) {
+ if (orientationRespectedWithInsets && handlesOrientationChangeFromDescendant(
+ getOverrideOrientation())) {
// No need to letterbox because of fixed orientation. Display will handle
// fixed-orientation requests and a display rotation is enough to respect requested
// orientation with insets applied.
@@ -8915,7 +8961,7 @@
int activityWidth = containingAppWidth;
int activityHeight = containingAppHeight;
- if (containingRatio > desiredAspectRatio) {
+ if (containingRatio - desiredAspectRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
if (containingAppWidth < containingAppHeight) {
// Width is the shorter side, so we use that to figure-out what the max. height
// should be given the aspect ratio.
@@ -8925,7 +8971,7 @@
// should be given the aspect ratio.
activityWidth = (int) ((activityHeight * desiredAspectRatio) + 0.5f);
}
- } else if (containingRatio < desiredAspectRatio) {
+ } else if (desiredAspectRatio - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
boolean adjustWidth;
switch (getRequestedConfigurationOrientation()) {
case ORIENTATION_LANDSCAPE:
@@ -9003,7 +9049,8 @@
}
if (info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY)
- && !ActivityInfo.isFixedOrientationPortrait(getRequestedOrientation())) {
+ && !ActivityInfo.isFixedOrientationPortrait(
+ getOverrideOrientation())) {
return info.getMinAspectRatio();
}
@@ -9998,6 +10045,7 @@
isLandscape ? shortSide : longSide);
}
+ // TODO(b/267151420): Explore removing getContainerBounds() from CompatDisplayInsets.
/** Gets the horizontal centered container bounds for size compatibility mode. */
void getContainerBounds(Rect outAppBounds, Rect outBounds, int rotation, int orientation,
boolean orientationRequested, boolean isFixedToUserRotation) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 4cbb2cd..58b9e3e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4724,6 +4724,7 @@
mTaskChangeNotificationController.notifyTaskFocusChanged(prevTask.mTaskId, false);
}
mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true);
+ mTaskSupervisor.mRecentTasks.add(task);
}
applyUpdateLockStateLocked(r);
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index fd6c974..b160af6a 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -98,7 +98,7 @@
throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
}
return mService.getRecentTasks().createRecentTaskInfo(task,
- false /* stripExtras */);
+ false /* stripExtras */, true /* getTasksAllowed */);
} finally {
Binder.restoreCallingIdentity(origId);
}
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index abaa363..0ea6157 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -892,7 +892,7 @@
*
* TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled.
*/
- private static boolean isTaskViewTask(WindowContainer wc) {
+ static boolean isTaskViewTask(WindowContainer wc) {
// We use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and
// it is not guaranteed to work this logic in the future version.
return wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer;
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
index a6f8557..7d9a4ec 100644
--- a/services/core/java/com/android/server/wm/DeviceStateController.java
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -16,80 +16,107 @@
package com.android.server.wm;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.os.Handler;
import android.os.HandlerExecutor;
+import com.android.internal.R;
import com.android.internal.util.ArrayUtils;
+import java.util.ArrayList;
+import java.util.List;
import java.util.function.Consumer;
/**
- * Class that registers callbacks with the {@link DeviceStateManager} and
- * responds to fold state changes by forwarding such events to a delegate.
+ * Class that registers callbacks with the {@link DeviceStateManager} and responds to device
+ * changes.
*/
-final class DeviceStateController {
+final class DeviceStateController implements DeviceStateManager.DeviceStateCallback {
+
+ @NonNull
private final DeviceStateManager mDeviceStateManager;
- private final Context mContext;
+ @NonNull
+ private final int[] mOpenDeviceStates;
+ @NonNull
+ private final int[] mHalfFoldedDeviceStates;
+ @NonNull
+ private final int[] mFoldedDeviceStates;
+ @NonNull
+ private final int[] mRearDisplayDeviceStates;
+ @NonNull
+ private final int[] mReverseRotationAroundZAxisStates;
+ @NonNull
+ private final List<Consumer<DeviceState>> mDeviceStateCallbacks = new ArrayList<>();
- private FoldStateListener mDeviceStateListener;
+ @Nullable
+ private DeviceState mLastDeviceState;
+ private int mCurrentState;
- public enum FoldState {
- UNKNOWN, OPEN, FOLDED, HALF_FOLDED
+ public enum DeviceState {
+ UNKNOWN, OPEN, FOLDED, HALF_FOLDED, REAR,
}
- DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) {
- mContext = context;
- mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class);
+ DeviceStateController(@NonNull Context context, @NonNull Handler handler) {
+ mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+
+ mOpenDeviceStates = context.getResources()
+ .getIntArray(R.array.config_openDeviceStates);
+ mHalfFoldedDeviceStates = context.getResources()
+ .getIntArray(R.array.config_halfFoldedDeviceStates);
+ mFoldedDeviceStates = context.getResources()
+ .getIntArray(R.array.config_foldedDeviceStates);
+ mRearDisplayDeviceStates = context.getResources()
+ .getIntArray(R.array.config_rearDisplayDeviceStates);
+ mReverseRotationAroundZAxisStates = context.getResources()
+ .getIntArray(R.array.config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis);
+
if (mDeviceStateManager != null) {
- mDeviceStateListener = new FoldStateListener(mContext, delegate);
- mDeviceStateManager
- .registerCallback(new HandlerExecutor(handler),
- mDeviceStateListener);
+ mDeviceStateManager.registerCallback(new HandlerExecutor(handler), this);
}
}
void unregisterFromDeviceStateManager() {
- if (mDeviceStateListener != null) {
- mDeviceStateManager.unregisterCallback(mDeviceStateListener);
+ if (mDeviceStateManager != null) {
+ mDeviceStateManager.unregisterCallback(this);
}
}
+ void registerDeviceStateCallback(@NonNull Consumer<DeviceState> callback) {
+ mDeviceStateCallbacks.add(callback);
+ }
+
/**
- * A listener for half-fold device state events that dispatches state changes to a delegate.
+ * @return true if the rotation direction on the Z axis should be reversed.
*/
- static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+ boolean shouldReverseRotationDirectionAroundZAxis() {
+ return ArrayUtils.contains(mReverseRotationAroundZAxisStates, mCurrentState);
+ }
- private final int[] mHalfFoldedDeviceStates;
- private final int[] mFoldedDeviceStates;
+ @Override
+ public void onStateChanged(int state) {
+ mCurrentState = state;
- @Nullable
- private FoldState mLastResult;
- private final Consumer<FoldState> mDelegate;
-
- FoldStateListener(Context context, Consumer<FoldState> delegate) {
- mFoldedDeviceStates = context.getResources().getIntArray(
- com.android.internal.R.array.config_foldedDeviceStates);
- mHalfFoldedDeviceStates = context.getResources().getIntArray(
- com.android.internal.R.array.config_halfFoldedDeviceStates);
- mDelegate = delegate;
+ final DeviceState deviceState;
+ if (ArrayUtils.contains(mHalfFoldedDeviceStates, state)) {
+ deviceState = DeviceState.HALF_FOLDED;
+ } else if (ArrayUtils.contains(mFoldedDeviceStates, state)) {
+ deviceState = DeviceState.FOLDED;
+ } else if (ArrayUtils.contains(mRearDisplayDeviceStates, state)) {
+ deviceState = DeviceState.REAR;
+ } else if (ArrayUtils.contains(mOpenDeviceStates, state)) {
+ deviceState = DeviceState.OPEN;
+ } else {
+ deviceState = DeviceState.UNKNOWN;
}
- @Override
- public void onStateChanged(int state) {
- final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state);
- FoldState result;
- if (halfFolded) {
- result = FoldState.HALF_FOLDED;
- } else {
- final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
- result = folded ? FoldState.FOLDED : FoldState.OPEN;
- }
- if (mLastResult == null || !mLastResult.equals(result)) {
- mLastResult = result;
- mDelegate.accept(result);
+ if (mLastDeviceState == null || !mLastDeviceState.equals(deviceState)) {
+ mLastDeviceState = deviceState;
+
+ for (Consumer<DeviceState> callback : mDeviceStateCallbacks) {
+ callback.accept(mLastDeviceState);
}
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 71fef05..dc6b425 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -94,7 +94,7 @@
DisplayArea(WindowManagerService wms, Type type, String name, int featureId) {
super(wms);
// TODO(display-area): move this up to ConfigurationContainer
- mOrientation = SCREEN_ORIENTATION_UNSET;
+ setOverrideOrientation(SCREEN_ORIENTATION_UNSET);
mType = type;
mName = name;
mFeatureId = featureId;
@@ -166,7 +166,8 @@
// If this is set to ignore the orientation request, we don't propagate descendant
// orientation request.
final int orientation = requestingContainer != null
- ? requestingContainer.mOrientation : SCREEN_ORIENTATION_UNSET;
+ ? requestingContainer.getOverrideOrientation()
+ : SCREEN_ORIENTATION_UNSET;
return !getIgnoreOrientationRequest(orientation)
&& super.onDescendantOrientationChanged(requestingContainer);
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 75d84ea..aede04b 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -27,6 +27,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -1125,14 +1126,18 @@
mWmService.mAtmService.getRecentTasks().getInputListener());
}
- mDisplayPolicy = new DisplayPolicy(mWmService, this);
- mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address);
+ mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH);
- mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
- newFoldState -> {
+ mDisplayPolicy = new DisplayPolicy(mWmService, this);
+ mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address,
+ mDeviceStateController);
+
+ final Consumer<DeviceStateController.DeviceState> deviceStateConsumer =
+ (@NonNull DeviceStateController.DeviceState newFoldState) -> {
mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
mDisplayRotation.foldStateChanged(newFoldState);
- });
+ };
+ mDeviceStateController.registerDeviceStateCallback(deviceStateConsumer);
mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat(
R.dimen.config_closeToSquareDisplayMaxAspectRatio);
@@ -1572,7 +1577,8 @@
// If display rotation class tells us that it doesn't consider app requested orientation,
// this display won't rotate just because of an app changes its requested orientation. Thus
// it indicates that this display chooses not to handle this request.
- final int orientation = requestingContainer != null ? requestingContainer.mOrientation
+ final int orientation = requestingContainer != null
+ ? requestingContainer.getOverrideOrientation()
: SCREEN_ORIENTATION_UNSET;
final boolean handled = handlesOrientationChangeFromDescendant(orientation);
if (config == null) {
@@ -1698,15 +1704,15 @@
if (mTransitionController.useShellTransitionsRotation()) {
return ROTATION_UNDEFINED;
}
+ final int activityOrientation = r.getOverrideOrientation();
if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM
- || getIgnoreOrientationRequest(r.mOrientation)) {
+ || getIgnoreOrientationRequest(activityOrientation)) {
return ROTATION_UNDEFINED;
}
- if (r.mOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
+ if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
final ActivityRecord nextCandidate = getActivity(
- a -> a.mOrientation != SCREEN_ORIENTATION_UNSET
- && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND,
- r, false /* includeBoundary */, true /* traverseTopToBottom */);
+ a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
+ r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
if (nextCandidate != null) {
r = nextCandidate;
}
@@ -2723,6 +2729,15 @@
final int orientation = super.getOrientation();
if (!handlesOrientationChangeFromDescendant(orientation)) {
+ ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true);
+ if (topActivity != null && topActivity.mLetterboxUiController
+ .shouldUseDisplayLandscapeNaturalOrientation()) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is ignoring orientation request for %d, return %d"
+ + " following a per-app override for %s",
+ mDisplayId, orientation, SCREEN_ORIENTATION_LANDSCAPE, topActivity);
+ return SCREEN_ORIENTATION_LANDSCAPE;
+ }
mLastOrientationSource = null;
// Return SCREEN_ORIENTATION_UNSPECIFIED so that Display respect sensor rotation
ProtoLog.v(WM_DEBUG_ORIENTATION,
@@ -2974,7 +2989,7 @@
if (density == mInitialDisplayDensity) {
density = 0;
}
- mWmService.mDisplayWindowSettings.setForcedDensity(this, density, userId);
+ mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId);
}
/** @param mode {@link #FORCE_SCALING_MODE_AUTO} or {@link #FORCE_SCALING_MODE_DISABLED}. */
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 0bb4022..cf7d5d9 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -142,6 +142,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.internal.util.ScreenshotHelper;
+import com.android.internal.util.ScreenshotRequest;
import com.android.internal.util.function.TriConsumer;
import com.android.internal.view.AppearanceRegion;
import com.android.internal.widget.PointerLocationView;
@@ -2006,7 +2007,8 @@
dc.getDisplayPolicy().simulateLayoutDisplay(df);
final InsetsState insetsState = df.mInsetsState;
final Rect displayFrame = insetsState.getDisplayFrame();
- final Insets decor = calculateDecorInsetsWithInternalTypes(insetsState);
+ final Insets decor = insetsState.calculateInsets(displayFrame, DECOR_TYPES,
+ true /* ignoreVisibility */);
final Insets statusBar = insetsState.calculateInsets(displayFrame,
Type.statusBars(), true /* ignoreVisibility */);
mNonDecorInsets.set(decor.left, decor.top, decor.right, decor.bottom);
@@ -2038,17 +2040,8 @@
}
}
- // TODO (b/235842600): Use public type once we can treat task bar as navigation bar.
- static final int[] INTERNAL_DECOR_TYPES;
- static {
- final ArraySet<Integer> decorTypes = InsetsState.toInternalType(
- Type.displayCutout() | Type.navigationBars());
- decorTypes.remove(ITYPE_EXTRA_NAVIGATION_BAR);
- INTERNAL_DECOR_TYPES = new int[decorTypes.size()];
- for (int i = 0; i < INTERNAL_DECOR_TYPES.length; i++) {
- INTERNAL_DECOR_TYPES[i] = decorTypes.valueAt(i);
- }
- }
+
+ static final int DECOR_TYPES = Type.displayCutout() | Type.navigationBars();
private final DisplayContent mDisplayContent;
private final Info[] mInfoForRotation = new Info[4];
@@ -2075,20 +2068,6 @@
info.mNeedUpdate = true;
}
}
-
- // TODO (b/235842600): Remove this method once we can treat task bar as navigation bar.
- private static Insets calculateDecorInsetsWithInternalTypes(InsetsState state) {
- final Rect frame = state.getDisplayFrame();
- Insets insets = Insets.NONE;
- for (int i = INTERNAL_DECOR_TYPES.length - 1; i >= 0; i--) {
- final InsetsSource source = state.peekSource(INTERNAL_DECOR_TYPES[i]);
- if (source != null) {
- insets = Insets.max(source.calculateInsets(frame, true /* ignoreVisibility */),
- insets);
- }
- }
- return insets;
- }
}
/**
@@ -2666,8 +2645,9 @@
*/
public void takeScreenshot(int screenshotType, int source) {
if (mScreenshotHelper != null) {
- mScreenshotHelper.takeScreenshot(screenshotType,
- source, mHandler, null /* completionConsumer */);
+ ScreenshotRequest request =
+ new ScreenshotRequest.Builder(screenshotType, source).build();
+ mScreenshotHelper.takeScreenshot(request, mHandler, null /* completionConsumer */);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index e6d8b3d..3404279 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -41,6 +41,7 @@
import android.annotation.AnimRes;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ContentResolver;
@@ -117,6 +118,8 @@
private SettingsObserver mSettingsObserver;
@Nullable
private FoldController mFoldController;
+ @NonNull
+ private final DeviceStateController mDeviceStateController;
@ScreenOrientation
private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -218,21 +221,24 @@
private boolean mDemoRotationLock;
DisplayRotation(WindowManagerService service, DisplayContent displayContent,
- DisplayAddress displayAddress) {
+ DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController) {
this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(),
- service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock());
+ service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock(),
+ deviceStateController);
}
@VisibleForTesting
DisplayRotation(WindowManagerService service, DisplayContent displayContent,
DisplayAddress displayAddress, DisplayPolicy displayPolicy,
- DisplayWindowSettings displayWindowSettings, Context context, Object lock) {
+ DisplayWindowSettings displayWindowSettings, Context context, Object lock,
+ @NonNull DeviceStateController deviceStateController) {
mService = service;
mDisplayContent = displayContent;
mDisplayPolicy = displayPolicy;
mDisplayWindowSettings = displayWindowSettings;
mContext = context;
mLock = lock;
+ mDeviceStateController = deviceStateController;
isDefaultDisplay = displayContent.isDefaultDisplay;
mCompatPolicyForImmersiveApps = initImmersiveAppCompatPolicy(service, displayContent);
@@ -243,11 +249,13 @@
mDeskDockRotation = readRotation(R.integer.config_deskDockRotation);
mUndockedHdmiRotation = readRotation(R.integer.config_undockedHdmiRotation);
- mRotation = readDefaultDisplayRotation(displayAddress);
+ int defaultRotation = readDefaultDisplayRotation(displayAddress);
+ mRotation = defaultRotation;
if (isDefaultDisplay) {
final Handler uiHandler = UiThread.getHandler();
- mOrientationListener = new OrientationListener(mContext, uiHandler);
+ mOrientationListener =
+ new OrientationListener(mContext, uiHandler, defaultRotation);
mOrientationListener.setCurrentRotation(mRotation);
mSettingsObserver = new SettingsObserver(uiHandler);
mSettingsObserver.observe();
@@ -1137,6 +1145,15 @@
int sensorRotation = mOrientationListener != null
? mOrientationListener.getProposedRotation() // may be -1
: -1;
+ if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) {
+ // Flipping 270 and 90 has the same effect as changing the direction which rotation is
+ // applied.
+ if (sensorRotation == Surface.ROTATION_90) {
+ sensorRotation = Surface.ROTATION_270;
+ } else if (sensorRotation == Surface.ROTATION_270) {
+ sensorRotation = Surface.ROTATION_90;
+ }
+ }
mLastSensorRotation = sensorRotation;
if (sensorRotation < 0) {
sensorRotation = lastRotation;
@@ -1573,7 +1590,7 @@
proto.end(token);
}
- boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+ boolean isDeviceInPosture(DeviceStateController.DeviceState state, boolean isTabletop) {
if (mFoldController == null) return false;
return mFoldController.isDeviceInPosture(state, isTabletop);
}
@@ -1585,10 +1602,10 @@
/**
* Called by the DeviceStateManager callback when the device state changes.
*/
- void foldStateChanged(DeviceStateController.FoldState foldState) {
+ void foldStateChanged(DeviceStateController.DeviceState deviceState) {
if (mFoldController != null) {
synchronized (mLock) {
- mFoldController.foldStateChanged(foldState);
+ mFoldController.foldStateChanged(deviceState);
}
}
}
@@ -1596,8 +1613,8 @@
private class FoldController {
@Surface.Rotation
private int mHalfFoldSavedRotation = -1; // No saved rotation
- private DeviceStateController.FoldState mFoldState =
- DeviceStateController.FoldState.UNKNOWN;
+ private DeviceStateController.DeviceState mDeviceState =
+ DeviceStateController.DeviceState.UNKNOWN;
private boolean mInHalfFoldTransition = false;
private final boolean mIsDisplayAlwaysSeparatingHinge;
private final Set<Integer> mTabletopRotations;
@@ -1637,32 +1654,33 @@
R.bool.config_isDisplayHingeAlwaysSeparating);
}
- boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
- if (state != mFoldState) {
+ boolean isDeviceInPosture(DeviceStateController.DeviceState state, boolean isTabletop) {
+ if (state != mDeviceState) {
return false;
}
- if (mFoldState == DeviceStateController.FoldState.HALF_FOLDED) {
+ if (mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED) {
return !(isTabletop ^ mTabletopRotations.contains(mRotation));
}
return true;
}
- DeviceStateController.FoldState getFoldState() {
- return mFoldState;
+ DeviceStateController.DeviceState getFoldState() {
+ return mDeviceState;
}
boolean isSeparatingHinge() {
- return mFoldState == DeviceStateController.FoldState.HALF_FOLDED
- || (mFoldState == DeviceStateController.FoldState.OPEN
+ return mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED
+ || (mDeviceState == DeviceStateController.DeviceState.OPEN
&& mIsDisplayAlwaysSeparatingHinge);
}
boolean overrideFrozenRotation() {
- return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
+ return mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED;
}
boolean shouldRevertOverriddenRotation() {
- return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open.
+ // When transitioning to open.
+ return mDeviceState == DeviceStateController.DeviceState.OPEN
&& mInHalfFoldTransition
&& mHalfFoldSavedRotation != -1 // Ignore if we've already reverted.
&& mUserRotationMode
@@ -1676,30 +1694,30 @@
return savedRotation;
}
- void foldStateChanged(DeviceStateController.FoldState newState) {
+ void foldStateChanged(DeviceStateController.DeviceState newState) {
ProtoLog.v(WM_DEBUG_ORIENTATION,
"foldStateChanged: displayId %d, halfFoldStateChanged %s, "
+ "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, "
+ "mLastOrientation: %d, mRotation: %d",
mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation,
mUserRotation, mLastSensorRotation, mLastOrientation, mRotation);
- if (mFoldState == DeviceStateController.FoldState.UNKNOWN) {
- mFoldState = newState;
+ if (mDeviceState == DeviceStateController.DeviceState.UNKNOWN) {
+ mDeviceState = newState;
return;
}
- if (newState == DeviceStateController.FoldState.HALF_FOLDED
- && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) {
+ if (newState == DeviceStateController.DeviceState.HALF_FOLDED
+ && mDeviceState != DeviceStateController.DeviceState.HALF_FOLDED) {
// The device has transitioned to HALF_FOLDED state: save the current rotation and
// update the device rotation.
mHalfFoldSavedRotation = mRotation;
- mFoldState = newState;
+ mDeviceState = newState;
// Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will
// return true, so rotation is unlocked.
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
} else {
mInHalfFoldTransition = true;
- mFoldState = newState;
+ mDeviceState = newState;
// Tell the device to update its orientation.
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
@@ -1719,8 +1737,9 @@
private class OrientationListener extends WindowOrientationListener implements Runnable {
transient boolean mEnabled;
- OrientationListener(Context context, Handler handler) {
- super(context, handler);
+ OrientationListener(Context context, Handler handler,
+ @Surface.Rotation int defaultRotation) {
+ super(context, handler, defaultRotation);
}
@Override
@@ -1822,7 +1841,7 @@
final long mTimestamp = System.currentTimeMillis();
final int mHalfFoldSavedRotation;
final boolean mInHalfFoldTransition;
- final DeviceStateController.FoldState mFoldState;
+ final DeviceStateController.DeviceState mDeviceState;
@Nullable final String mDisplayRotationCompatPolicySummary;
Record(DisplayRotation dr, int fromRotation, int toRotation) {
@@ -1843,8 +1862,9 @@
if (source != null) {
mLastOrientationSource = source.toString();
final WindowState w = source.asWindowState();
- mSourceOrientation =
- w != null ? w.mAttrs.screenOrientation : source.mOrientation;
+ mSourceOrientation = w != null
+ ? w.mAttrs.screenOrientation
+ : source.getOverrideOrientation();
} else {
mLastOrientationSource = null;
mSourceOrientation = SCREEN_ORIENTATION_UNSET;
@@ -1852,11 +1872,11 @@
if (dr.mFoldController != null) {
mHalfFoldSavedRotation = dr.mFoldController.mHalfFoldSavedRotation;
mInHalfFoldTransition = dr.mFoldController.mInHalfFoldTransition;
- mFoldState = dr.mFoldController.mFoldState;
+ mDeviceState = dr.mFoldController.mDeviceState;
} else {
mHalfFoldSavedRotation = NO_FOLD_CONTROLLER;
mInHalfFoldTransition = false;
- mFoldState = DeviceStateController.FoldState.UNKNOWN;
+ mDeviceState = DeviceStateController.DeviceState.UNKNOWN;
}
mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
? null
@@ -1882,7 +1902,7 @@
pw.println(prefix + " halfFoldSavedRotation="
+ mHalfFoldSavedRotation
+ " mInHalfFoldTransition=" + mInHalfFoldTransition
- + " mFoldState=" + mFoldState);
+ + " mFoldState=" + mDeviceState);
}
if (mDisplayRotationCompatPolicySummary != null) {
pw.println(prefix + mDisplayRotationCompatPolicySummary);
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index ba0413d..3ffb2fa 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -203,8 +203,11 @@
|| !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
return;
}
- boolean cycleThroughStop = mWmService.mLetterboxConfiguration
- .isCameraCompatRefreshCycleThroughStopEnabled();
+ boolean cycleThroughStop =
+ mWmService.mLetterboxConfiguration
+ .isCameraCompatRefreshCycleThroughStopEnabled()
+ && !activity.mLetterboxUiController
+ .shouldRefreshActivityViaPauseForCameraCompat();
try {
activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
ProtoLog.v(WM_DEBUG_STATES,
@@ -255,7 +258,8 @@
Configuration lastReportedConfig) {
return newConfig.windowConfiguration.getDisplayRotation()
!= lastReportedConfig.windowConfiguration.getDisplayRotation()
- && isTreatmentEnabledForActivity(activity);
+ && isTreatmentEnabledForActivity(activity)
+ && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat();
}
/**
@@ -292,9 +296,10 @@
&& activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
// "locked" and "nosensor" values are often used by camera apps that can't
// handle dynamic changes so we shouldn't force rotate them.
- && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
- && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
- && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+ && activity.getOverrideOrientation() != SCREEN_ORIENTATION_NOSENSOR
+ && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED
+ && mCameraIdPackageBiMap.containsPackageName(activity.packageName)
+ && activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
}
private synchronized void notifyCameraOpened(
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 8763900..2e8c9ac 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -77,14 +77,14 @@
mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
}
- void setForcedDensity(DisplayContent displayContent, int density, int userId) {
- if (displayContent.isDefaultDisplay) {
+ void setForcedDensity(DisplayInfo info, int density, int userId) {
+ if (info.displayId == Display.DEFAULT_DISPLAY) {
final String densityString = density == 0 ? "" : Integer.toString(density);
Settings.Secure.putStringForUser(mService.mContext.getContentResolver(),
Settings.Secure.DISPLAY_DENSITY_FORCED, densityString, userId);
}
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+ final DisplayInfo displayInfo = info;
final SettingsProvider.SettingsEntry overrideSettings =
mSettingsProvider.getOverrideSettings(displayInfo);
overrideSettings.mForcedDensity = density;
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index f916ee4..b64420a 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -18,6 +18,7 @@
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.LetterboxConfigurationDeviceConfig.KEY_ALLOW_IGNORE_ORIENTATION_REQUEST;
import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
import android.annotation.IntDef;
@@ -197,6 +198,12 @@
// Whether using split screen aspect ratio as a default aspect ratio for unresizable apps.
private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled;
+ // Whether using display aspect ratio as a default aspect ratio for all letterboxed apps.
+ // mIsSplitScreenAspectRatioForUnresizableAppsEnabled and
+ // config_letterboxDefaultMinAspectRatioForUnresizableApps take priority over this for
+ // unresizable apps
+ private boolean mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
+
// Whether letterboxing strategy is enabled for translucent activities. If {@value false}
// all the feature is disabled
private boolean mTranslucentLetterboxingEnabled;
@@ -288,6 +295,9 @@
R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps));
mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
+ mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = mContext.getResources()
+ .getBoolean(R.bool
+ .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsEnabledForTranslucentActivities);
mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
@@ -302,12 +312,23 @@
mDeviceConfig.updateFlagActiveStatus(
/* isActive */ mIsDisplayRotationImmersiveAppCompatPolicyEnabled,
/* key */ KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY);
+ mDeviceConfig.updateFlagActiveStatus(
+ /* isActive */ true,
+ /* key */ KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
}
/**
+ * Whether enabling ignoreOrientationRequest is allowed on the device. This value is controlled
+ * via {@link android.provider.DeviceConfig}.
+ */
+ boolean isIgnoreOrientationRequestAllowed() {
+ return mDeviceConfig.getFlag(KEY_ALLOW_IGNORE_ORIENTATION_REQUEST);
+ }
+
+ /**
* Overrides the aspect ratio of letterbox for fixed orientation. If given value is <= {@link
* #MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO}, both it and a value of {@link
* com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and
@@ -943,6 +964,13 @@
}
/**
+ * Whether using display aspect ratio as a default aspect ratio for all letterboxed apps.
+ */
+ boolean getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox() {
+ return mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
+ }
+
+ /**
* Overrides whether using split screen aspect ratio as a default aspect ratio for unresizable
* apps.
*/
@@ -951,6 +979,14 @@
}
/**
+ * Overrides whether using display aspect ratio as a default aspect ratio for all letterboxed
+ * apps.
+ */
+ void setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(boolean enabled) {
+ mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = enabled;
+ }
+
+ /**
* Resets whether using split screen aspect ratio as a default aspect ratio for unresizable
* apps {@link R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled}.
*/
@@ -959,6 +995,16 @@
R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
}
+ /**
+ * Resets whether using display aspect ratio as a default aspect ratio for all letterboxed
+ * apps {@link R.bool.config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled}.
+ */
+ void resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox() {
+ mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox = mContext.getResources()
+ .getBoolean(R.bool
+ .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled);
+ }
+
boolean isTranslucentLetterboxingEnabled() {
return mTranslucentLetterboxingOverrideEnabled || (mTranslucentLetterboxingEnabled
&& isTranslucentLetterboxingAllowed());
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
index cf123a1..3f067e3 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java
@@ -38,10 +38,16 @@
private static final boolean DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY =
true;
+ static final String KEY_ALLOW_IGNORE_ORIENTATION_REQUEST =
+ "allow_ignore_orientation_request";
+ private static final boolean DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST = true;
+
@VisibleForTesting
static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of(
KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
- DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY
+ DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY,
+ KEY_ALLOW_IGNORE_ORIENTATION_REQUEST,
+ DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST
);
// Whether enabling rotation compat policy for immersive apps that prevents auto rotation
@@ -52,6 +58,10 @@
private boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY;
+ // Whether enabling ignoreOrientationRequest is allowed on the device.
+ private boolean mIsAllowIgnoreOrientationRequest =
+ DEFAULT_VALUE_ALLOW_IGNORE_ORIENTATION_REQUEST;
+
// Set of active device configs that need to be updated in
// DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged.
private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>();
@@ -93,6 +103,8 @@
switch (key) {
case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY:
return mIsDisplayRotationImmersiveAppCompatPolicyEnabled;
+ case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
+ return mIsAllowIgnoreOrientationRequest;
default:
throw new AssertionError("Unexpected flag name: " + key);
}
@@ -108,6 +120,10 @@
mIsDisplayRotationImmersiveAppCompatPolicyEnabled =
getDeviceConfig(key, defaultValue);
break;
+ case KEY_ALLOW_IGNORE_ORIENTATION_REQUEST:
+ mIsAllowIgnoreOrientationRequest =
+ getDeviceConfig(key, defaultValue);
+ break;
default:
throw new AssertionError("Unexpected flag name: " + key);
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 0c8a645..c5a50ca 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -17,12 +17,30 @@
package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.isFixedOrientation;
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
import static android.content.pm.ActivityInfo.screenOrientationToString;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
@@ -56,6 +74,9 @@
import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+
import android.annotation.Nullable;
import android.app.ActivityManager.TaskDescription;
import android.content.pm.ActivityInfo.ScreenOrientation;
@@ -99,6 +120,40 @@
private final ActivityRecord mActivityRecord;
+ /**
+ * Taskbar expanded height. Used to determine when to crop an app window to display the
+ * rounded corners above the expanded taskbar.
+ */
+ private final float mExpandedTaskBarHeight;
+
+ // TODO(b/265576778): Cache other overrides as well.
+
+ // Corresponds to OVERRIDE_ANY_ORIENTATION
+ private final boolean mIsOverrideAnyOrientationEnabled;
+ // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT
+ private final boolean mIsOverrideToPortraitOrientationEnabled;
+ // Corresponds to OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR
+ private final boolean mIsOverrideToNosensorOrientationEnabled;
+ // Corresponds to OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE
+ private final boolean mIsOverrideToReverseLandscapeOrientationEnabled;
+ // Corresponds to OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION
+ private final boolean mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled;
+
+ // Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION
+ private final boolean mIsOverrideCameraCompatDisableForceRotationEnabled;
+ // Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH
+ private final boolean mIsOverrideCameraCompatDisableRefreshEnabled;
+ // Corresponds to OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE
+ private final boolean mIsOverrideCameraCompatEnableRefreshViaPauseEnabled;
+
+ // Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION
+ private final boolean mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled;
+
+ @Nullable
+ private final Boolean mBooleanPropertyAllowOrientationOverride;
+ @Nullable
+ private final Boolean mBooleanPropertyAllowDisplayOrientationOverride;
+
/*
* WindowContainerListener responsible to make translucent activities inherit
* constraints from the first opaque activity beneath them. It's null for not
@@ -132,6 +187,15 @@
@Nullable
private Letterbox mLetterbox;
+ @Nullable
+ private final Boolean mBooleanPropertyCameraCompatAllowForceRotation;
+
+ @Nullable
+ private final Boolean mBooleanPropertyCameraCompatAllowRefresh;
+
+ @Nullable
+ private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause;
+
// Whether activity "refresh" was requested but not finished in
// ActivityRecord#activityResumedLocked following the camera compat force rotation in
// DisplayRotationCompatPolicy.
@@ -154,12 +218,69 @@
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+ mBooleanPropertyCameraCompatAllowForceRotation =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true),
+ PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION);
+ mBooleanPropertyCameraCompatAllowRefresh =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true),
+ PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH);
+ mBooleanPropertyCameraCompatEnableRefreshViaPause =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true),
+ PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
+
+ mExpandedTaskBarHeight =
+ getResources().getDimensionPixelSize(R.dimen.taskbar_frame_height);
+
+ mBooleanPropertyAllowOrientationOverride =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ /* gatingCondition */ null,
+ PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+ mBooleanPropertyAllowDisplayOrientationOverride =
+ readComponentProperty(packageManager, mActivityRecord.packageName,
+ /* gatingCondition */ null,
+ PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE);
+
+ mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION);
+ mIsOverrideToPortraitOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT);
+ mIsOverrideToReverseLandscapeOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE);
+ mIsOverrideToNosensorOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
+ mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION);
+
+ mIsOverrideCameraCompatDisableForceRotationEnabled =
+ isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION);
+ mIsOverrideCameraCompatDisableRefreshEnabled =
+ isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH);
+ mIsOverrideCameraCompatEnableRefreshViaPauseEnabled =
+ isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
+
+ mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled =
+ isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION);
}
+ /**
+ * Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code
+ * propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the
+ * property isn't specified for the package.
+ *
+ * <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the
+ * property is unset. Particularly, when this returns {@code null}, {@link
+ * #shouldEnableWithOverrideAndProperty} will check the value of override for the final
+ * decision.
+ */
@Nullable
private static Boolean readComponentProperty(PackageManager packageManager, String packageName,
- BooleanSupplier gatingCondition, String propertyName) {
- if (!gatingCondition.getAsBoolean()) {
+ @Nullable BooleanSupplier gatingCondition, String propertyName) {
+ if (gatingCondition != null && !gatingCondition.getAsBoolean()) {
return null;
}
try {
@@ -210,15 +331,11 @@
* </ul>
*/
boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
- if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) {
- return false;
- }
- if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) {
- return false;
- }
- if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation)
- && !mActivityRecord.info.isChangeEnabled(
- OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) {
+ if (!shouldEnableWithOverrideAndProperty(
+ /* gatingCondition */ mLetterboxConfiguration
+ ::isPolicyForIgnoringRequestedOrientationEnabled,
+ mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled,
+ mBooleanPropertyIgnoreRequestedOrientation)) {
return false;
}
if (mIsRelauchingAfterRequestedOrientationChanged) {
@@ -262,6 +379,190 @@
mIsRefreshAfterRotationRequested = isRequested;
}
+ /**
+ * Whether should fix display orientation to landscape natural orientation when a task is
+ * fullscreen and the display is ignoring orientation requests.
+ *
+ * <p>This treatment is enabled when the following conditions are met:
+ * <ul>
+ * <li>Opt-out component property isn't enabled
+ * <li>Opt-in per-app override is enabled
+ * <li>Task is in fullscreen.
+ * <li>{@link DisplayContent#getIgnoreOrientationRequest} is enabled
+ * <li>Natural orientation of the display is landscape.
+ * </ul>
+ */
+ boolean shouldUseDisplayLandscapeNaturalOrientation() {
+ return shouldEnableWithOptInOverrideAndOptOutProperty(
+ /* gatingCondition */ () -> mActivityRecord.mDisplayContent != null
+ && mActivityRecord.getTask() != null
+ && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest()
+ && !mActivityRecord.getTask().inMultiWindowMode()
+ && mActivityRecord.mDisplayContent.getNaturalOrientation()
+ == ORIENTATION_LANDSCAPE,
+ mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled,
+ mBooleanPropertyAllowDisplayOrientationOverride);
+ }
+
+ @ScreenOrientation
+ int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
+ if (FALSE.equals(mBooleanPropertyAllowOrientationOverride)) {
+ return candidate;
+ }
+
+ if (mIsOverrideToReverseLandscapeOrientationEnabled
+ && (isFixedOrientationLandscape(candidate) || mIsOverrideAnyOrientationEnabled)) {
+ Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ + mActivityRecord + " is overridden to "
+ + screenOrientationToString(SCREEN_ORIENTATION_REVERSE_LANDSCAPE));
+ return SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ }
+
+ if (!mIsOverrideAnyOrientationEnabled && isFixedOrientation(candidate)) {
+ return candidate;
+ }
+
+ if (mIsOverrideToPortraitOrientationEnabled) {
+ Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ + mActivityRecord + " is overridden to "
+ + screenOrientationToString(SCREEN_ORIENTATION_PORTRAIT));
+ return SCREEN_ORIENTATION_PORTRAIT;
+ }
+
+ if (mIsOverrideToNosensorOrientationEnabled) {
+ Slog.w(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for "
+ + mActivityRecord + " is overridden to "
+ + screenOrientationToString(SCREEN_ORIENTATION_NOSENSOR));
+ return SCREEN_ORIENTATION_NOSENSOR;
+ }
+
+ return candidate;
+ }
+
+ /**
+ * Whether activity is eligible for activity "refresh" after camera compat force rotation
+ * treatment. See {@link DisplayRotationCompatPolicy} for context.
+ *
+ * <p>This treatment is enabled when the following conditions are met:
+ * <ul>
+ * <li>Flag gating the camera compat treatment is enabled.
+ * <li>Activity isn't opted out by the device manufacturer with override or by the app
+ * developers with the component property.
+ * </ul>
+ */
+ boolean shouldRefreshActivityForCameraCompat() {
+ return shouldEnableWithOptOutOverrideAndProperty(
+ /* gatingCondition */ () -> mLetterboxConfiguration
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+ mIsOverrideCameraCompatDisableRefreshEnabled,
+ mBooleanPropertyCameraCompatAllowRefresh);
+ }
+
+ /**
+ * Whether activity should be "refreshed" after the camera compat force rotation treatment
+ * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped
+ * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context.
+ *
+ * <p>This treatment is enabled when the following conditions are met:
+ * <ul>
+ * <li>Flag gating the camera compat treatment is enabled.
+ * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the
+ * component property by the app developers.
+ * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device
+ * manufacturer with override / by the app developers with the component property.
+ * </ul>
+ */
+ boolean shouldRefreshActivityViaPauseForCameraCompat() {
+ return shouldEnableWithOverrideAndProperty(
+ /* gatingCondition */ () -> mLetterboxConfiguration
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+ mIsOverrideCameraCompatEnableRefreshViaPauseEnabled,
+ mBooleanPropertyCameraCompatEnableRefreshViaPause);
+ }
+
+ /**
+ * Whether activity is eligible for camera compat force rotation treatment. See {@link
+ * DisplayRotationCompatPolicy} for context.
+ *
+ * <p>This treatment is enabled when the following conditions are met:
+ * <ul>
+ * <li>Flag gating the camera compat treatment is enabled.
+ * <li>Activity isn't opted out by the device manufacturer with override or by the app
+ * developers with the component property.
+ * </ul>
+ */
+ boolean shouldForceRotateForCameraCompat() {
+ return shouldEnableWithOptOutOverrideAndProperty(
+ /* gatingCondition */ () -> mLetterboxConfiguration
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+ mIsOverrideCameraCompatDisableForceRotationEnabled,
+ mBooleanPropertyCameraCompatAllowForceRotation);
+ }
+
+ private boolean isCompatChangeEnabled(long overrideChangeId) {
+ return mActivityRecord.info.isChangeEnabled(overrideChangeId);
+ }
+
+ /**
+ * Returns {@code true} when the following conditions are met:
+ * <ul>
+ * <li>{@code gatingCondition} isn't {@code false}
+ * <li>OEM didn't opt out with a per-app override
+ * <li>App developers didn't opt out with a component {@code property}
+ * </ul>
+ *
+ * <p>This is used for the treatments that are enabled based with the heuristic but can be
+ * disabled on per-app basis by OEMs or app developers.
+ */
+ private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition,
+ boolean isOverrideChangeEnabled, Boolean property) {
+ if (!gatingCondition.getAsBoolean()) {
+ return false;
+ }
+ return !FALSE.equals(property) && !isOverrideChangeEnabled;
+ }
+
+ /**
+ * Returns {@code true} when the following conditions are met:
+ * <ul>
+ * <li>{@code gatingCondition} isn't {@code false}
+ * <li>OEM did opt in with a per-app override
+ * <li>App developers didn't opt out with a component {@code property}
+ * </ul>
+ *
+ * <p>This is used for the treatments that are enabled based with the heuristic but can be
+ * disabled on per-app basis by OEMs or app developers.
+ */
+ private boolean shouldEnableWithOptInOverrideAndOptOutProperty(BooleanSupplier gatingCondition,
+ boolean isOverrideChangeEnabled, Boolean property) {
+ if (!gatingCondition.getAsBoolean()) {
+ return false;
+ }
+ return !FALSE.equals(property) && isOverrideChangeEnabled;
+ }
+
+ /**
+ * Returns {@code true} when the following conditions are met:
+ * <ul>
+ * <li>{@code gatingCondition} isn't {@code false}
+ * <li>App developers didn't opt out with a component {@code property}
+ * <li>App developers opted in with a component {@code property} or an OEM opted in with a
+ * per-app override
+ * </ul>
+ *
+ * <p>This is used for the treatments that are enabled only on per-app basis.
+ */
+ private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition,
+ boolean isOverrideChangeEnabled, Boolean property) {
+ if (!gatingCondition.getAsBoolean()) {
+ return false;
+ }
+ if (FALSE.equals(property)) {
+ return false;
+ }
+ return TRUE.equals(property) || isOverrideChangeEnabled;
+ }
+
boolean hasWallpaperBackgroundForLetterbox() {
return mShowWallpaperForLetterboxBackground;
}
@@ -283,7 +584,7 @@
if (w == null) {
return;
}
- adjustBoundsForTaskbar(w, outBounds);
+ adjustBoundsIfNeeded(w, outBounds);
} else {
outBounds.setEmpty();
}
@@ -326,13 +627,13 @@
if (w == null || winHint != null && w != winHint) {
return;
}
- updateRoundedCorners(w);
+ updateRoundedCornersIfNeeded(w);
// If there is another main window that is not an application-starting window, we should
// update rounded corners for it as well, to avoid flickering rounded corners.
final WindowState nonStartingAppW = mActivityRecord.findMainWindow(
/* includeStartingApp= */ false);
if (nonStartingAppW != null && nonStartingAppW != w) {
- updateRoundedCorners(nonStartingAppW);
+ updateRoundedCornersIfNeeded(nonStartingAppW);
}
updateWallpaperForLetterbox(w);
@@ -394,7 +695,7 @@
// Note that we check the task rather than the parent as with ActivityEmbedding the parent might
// be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
// actually fullscreen.
- private boolean isDisplayFullScreenAndInPosture(DeviceStateController.FoldState state,
+ private boolean isDisplayFullScreenAndInPosture(DeviceStateController.DeviceState state,
boolean isTabletop) {
Task task = mActivityRecord.getTask();
return mActivityRecord.mDisplayContent != null
@@ -420,7 +721,7 @@
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
boolean bookMode = isDisplayFullScreenAndInPosture(
- DeviceStateController.FoldState.HALF_FOLDED, false /* isTabletop */);
+ DeviceStateController.DeviceState.HALF_FOLDED, false /* isTabletop */);
return isHorizontalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
@@ -432,7 +733,7 @@
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
boolean tabletopMode = isDisplayFullScreenAndInPosture(
- DeviceStateController.FoldState.HALF_FOLDED, true /* isTabletop */);
+ DeviceStateController.DeviceState.HALF_FOLDED, true /* isTabletop */);
return isVerticalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
@@ -445,7 +746,7 @@
? getSplitScreenAspectRatio()
: mActivityRecord.shouldCreateCompatDisplayInsets()
? getDefaultMinAspectRatioForUnresizableApps()
- : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+ : getDefaultMinAspectRatio();
}
private float getDefaultMinAspectRatioForUnresizableApps() {
@@ -454,7 +755,7 @@
return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
> MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
- : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+ : getDefaultMinAspectRatio();
}
return getSplitScreenAspectRatio();
@@ -482,6 +783,16 @@
return computeAspectRatio(bounds);
}
+ private float getDefaultMinAspectRatio() {
+ final DisplayContent displayContent = mActivityRecord.getDisplayContent();
+ if (displayContent == null
+ || !mLetterboxConfiguration
+ .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
+ return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+ }
+ return computeAspectRatio(new Rect(displayContent.getBounds()));
+ }
+
Resources getResources() {
return mActivityRecord.mWmService.mContext.getResources();
}
@@ -575,6 +886,7 @@
* <li>Activity is portrait-only.
* <li>Fullscreen window in landscape device orientation.
* <li>Horizontal Reachability is enabled.
+ * <li>Activity fills parent vertically.
* </ul>
*/
private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
@@ -582,10 +894,14 @@
&& parentConfiguration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_FULLSCREEN
&& (parentConfiguration.orientation == ORIENTATION_LANDSCAPE
- && mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT);
+ && mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT)
+ // Check whether the activity fills the parent vertically.
+ && parentConfiguration.windowConfiguration.getBounds().height()
+ == mActivityRecord.getBounds().height();
}
- private boolean isHorizontalReachabilityEnabled() {
+ @VisibleForTesting
+ boolean isHorizontalReachabilityEnabled() {
return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
}
@@ -597,6 +913,7 @@
* <li>Activity is landscape-only.
* <li>Fullscreen window in portrait device orientation.
* <li>Vertical Reachability is enabled.
+ * <li>Activity fills parent horizontally.
* </ul>
*/
private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
@@ -604,10 +921,14 @@
&& parentConfiguration.windowConfiguration.getWindowingMode()
== WINDOWING_MODE_FULLSCREEN
&& (parentConfiguration.orientation == ORIENTATION_PORTRAIT
- && mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE);
+ && mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE)
+ // Check whether the activity fills the parent horizontally.
+ && parentConfiguration.windowConfiguration.getBounds().width()
+ == mActivityRecord.getBounds().width();
}
- private boolean isVerticalReachabilityEnabled() {
+ @VisibleForTesting
+ boolean isVerticalReachabilityEnabled() {
return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration());
}
@@ -616,8 +937,8 @@
return isSurfaceReadyAndVisible(mainWindow) && 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 backgroud.
- && (mainWindow.mAttrs.flags & FLAG_SHOW_WALLPAPER) == 0;
+ // activity is using blurred wallpaper for letterbox background.
+ && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0;
}
@VisibleForTesting
@@ -669,106 +990,107 @@
return mLetterboxConfiguration.getLetterboxBackgroundColor();
}
- private void updateRoundedCorners(WindowState mainWindow) {
+ private void updateRoundedCornersIfNeeded(final WindowState mainWindow) {
final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
- if (windowSurface != null && windowSurface.isValid()) {
- final Transaction transaction = mActivityRecord.getSyncTransaction();
-
- 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.
- transaction
- .setWindowCrop(windowSurface, null)
- .setCornerRadius(windowSurface, 0);
- return;
- }
-
- Rect cropBounds = null;
-
- if (hasVisibleTaskbar(mainWindow)) {
- cropBounds = new Rect(mActivityRecord.getBounds());
-
- // Rounded corners should be displayed above the taskbar.
- // It is important to call adjustBoundsForTaskbarUnchecked before offsetTo
- // because taskbar bounds are in screen coordinates
- adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
-
- // Activity bounds are in screen coordinates while (0,0) for activity's surface
- // control is at the top left corner of an app window so offsetting bounds
- // accordingly.
- cropBounds.offsetTo(0, 0);
- }
-
- transaction
- .setWindowCrop(windowSurface, cropBounds)
- .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
+ 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(WindowState mainWindow) {
- final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
+ @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());
+
+ // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo}
+ // because taskbar bounds used in {@link #adjustBoundsIfNeeded}
+ // are in screen coordinates
+ adjustBoundsIfNeeded(mainWindow, cropBounds);
+
+ // 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)
- && mLetterboxConfiguration.isLetterboxActivityCornersRounded()
- && taskbarInsetsSource != null;
+ && mLetterboxConfiguration.isLetterboxActivityCornersRounded();
}
// 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
+ // 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(WindowState mainWindow) {
- if (!requiresRoundedCorners(mainWindow)) {
+ int getRoundedCornersRadius(final WindowState mainWindow) {
+ if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
return 0;
}
+ final int radius;
if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
- return mLetterboxConfiguration.getLetterboxActivityCornersRadius();
+ radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius();
+ } else {
+ final InsetsState insetsState = mainWindow.getInsetsState();
+ radius = Math.min(
+ getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
+ getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
}
- final InsetsState insetsState = mainWindow.getInsetsState();
- return 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;
}
/**
- * Returns whether the taskbar is visible. Returns false if the window is in immersive mode,
- * since the user can swipe to show/hide the taskbar as an overlay.
+ * Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
*/
- private boolean hasVisibleTaskbar(WindowState mainWindow) {
- final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
-
- return taskbarInsetsSource != null
- && taskbarInsetsSource.isVisible();
+ @VisibleForTesting
+ @Nullable
+ InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) {
+ final InsetsSource taskbar = mainWindow.getInsetsState().peekSource(
+ InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ if (taskbar != null && taskbar.isVisible()
+ && taskbar.getFrame().height() >= mExpandedTaskBarHeight) {
+ return taskbar;
+ }
+ return null;
}
- private InsetsSource getTaskbarInsetsSource(WindowState mainWindow) {
- final InsetsState insetsState = mainWindow.getInsetsState();
- return insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
- }
-
- private void adjustBoundsForTaskbar(WindowState mainWindow, Rect bounds) {
+ private void adjustBoundsIfNeeded(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.
- if (hasVisibleTaskbar(mainWindow)) {
- adjustBoundsForTaskbarUnchecked(mainWindow, bounds);
+ // 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 void adjustBoundsForTaskbarUnchecked(WindowState mainWindow, Rect bounds) {
- // Rounded corners should be displayed above the taskbar.
- bounds.bottom =
- Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
- scaleIfNeeded(bounds);
+ final float scale = mainWindow.mInvGlobalScale;
+ if (scale != 1f && scale > 0f) {
+ bounds.scale(scale);
+ }
}
private int getInsetsStateCornerRadius(
@@ -875,6 +1197,9 @@
+ mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps());
pw.println(prefix + " isSplitScreenAspectRatioForUnresizableAppsEnabled="
+ mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+ pw.println(prefix + " isDisplayAspectRatioEnabledForFixedOrientationLetterbox="
+ + mLetterboxConfiguration
+ .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
}
/**
@@ -935,7 +1260,7 @@
int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
.getLetterboxPositionForHorizontalReachability(
isDisplayFullScreenAndInPosture(
- DeviceStateController.FoldState.HALF_FOLDED,
+ DeviceStateController.DeviceState.HALF_FOLDED,
false /* isTabletop */));
positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
letterboxPositionForHorizontalReachability);
@@ -943,7 +1268,7 @@
int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
.getLetterboxPositionForVerticalReachability(
isDisplayFullScreenAndInPosture(
- DeviceStateController.FoldState.HALF_FOLDED,
+ DeviceStateController.DeviceState.HALF_FOLDED,
true /* isTabletop */));
positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
letterboxPositionForVerticalReachability);
@@ -1007,10 +1332,9 @@
final ActivityRecord firstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity(
ActivityRecord::fillsParent, mActivityRecord, false /* includeBoundary */,
true /* traverseTopToBottom */);
- if (firstOpaqueActivityBeneath == null
- || mActivityRecord.launchedFromUid != firstOpaqueActivityBeneath.getUid()) {
+ if (firstOpaqueActivityBeneath == null) {
// We skip letterboxing if the translucent activity doesn't have any opaque
- // activities beneath of if it's launched from a different user (e.g. notification)
+ // activities beneath
return;
}
inheritConfiguration(firstOpaqueActivityBeneath);
@@ -1053,7 +1377,8 @@
// To avoid wrong behaviour, we're not forcing orientation for activities with not
// fixed orientation (e.g. permission dialogs).
return hasInheritedLetterboxBehavior()
- && mActivityRecord.mOrientation != SCREEN_ORIENTATION_UNSPECIFIED;
+ && mActivityRecord.getOverrideOrientation()
+ != SCREEN_ORIENTATION_UNSPECIFIED;
}
float getInheritedMinAspectRatio() {
@@ -1108,20 +1433,4 @@
mInheritedSizeCompatScale = 1f;
mInheritedCompatDisplayInsets = null;
}
-
- private void scaleIfNeeded(Rect bounds) {
- if (boundsNeedToScale()) {
- bounds.scale(1.0f / mActivityRecord.getCompatScale());
- }
- }
-
- private boolean boundsNeedToScale() {
- if (hasInheritedLetterboxBehavior()) {
- return mIsInheritedInSizeCompatMode
- && mInheritedSizeCompatScale < 1.0f;
- } else {
- return mActivityRecord.inSizeCompatMode()
- && mActivityRecord.getCompatScale() < 1.0f;
- }
- }
}
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index 30bdc34..2edb082 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -51,10 +51,10 @@
/**
* Called by the DeviceStateManager callback when the state changes.
*/
- void foldStateChanged(DeviceStateController.FoldState newFoldState) {
+ void foldStateChanged(DeviceStateController.DeviceState newDeviceState) {
// Ignore transitions to/from half-folded.
- if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return;
- mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED;
+ if (newDeviceState == DeviceStateController.DeviceState.HALF_FOLDED) return;
+ mIsFolded = newDeviceState == DeviceStateController.DeviceState.FOLDED;
}
/**
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 4860762..1fc061b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -976,7 +976,7 @@
continue;
}
- res.add(createRecentTaskInfo(task, true /* stripExtras */));
+ res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed));
}
return res;
}
@@ -1895,7 +1895,8 @@
/**
* Creates a new RecentTaskInfo from a Task.
*/
- ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) {
+ ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras,
+ boolean getTasksAllowed) {
final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
// If the recent Task is detached, we consider it will be re-attached to the default
// TaskDisplayArea because we currently only support recent overview in the default TDA.
@@ -1907,6 +1908,9 @@
rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID;
rti.persistentId = rti.taskId;
rti.lastSnapshotData.set(tr.mLastTaskSnapshotData);
+ if (!getTasksAllowed) {
+ Task.trimIneffectiveInfo(tr, rti);
+ }
// Fill in organized child task info for the task created by organizer.
if (tr.mCreatedByOrganizer) {
diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java
index 33f019e..4e339f1 100644
--- a/services/core/java/com/android/server/wm/RunningTasks.java
+++ b/services/core/java/com/android/server/wm/RunningTasks.java
@@ -177,6 +177,10 @@
}
// Fill in some deprecated values
rti.id = rti.taskId;
+
+ if (!mAllowed) {
+ Task.trimIneffectiveInfo(task, rti);
+ }
return rti;
}
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index ba89657..90cae0a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -609,7 +609,7 @@
*/
ActivityRecord mChildPipActivity;
- boolean mLastSurfaceShowing = true;
+ boolean mLastSurfaceShowing;
/**
* Tracks if a back gesture is in progress.
@@ -3327,12 +3327,17 @@
// We intend to let organizer manage task visibility but it doesn't
// have enough information until we finish shell transitions.
// In the mean time we do an easy fix here.
- final boolean show = isVisible() || isAnimating(TRANSITION | PARENTS | CHILDREN);
+ final boolean visible = isVisible();
+ final boolean show = visible || isAnimating(TRANSITION | PARENTS | CHILDREN);
if (mSurfaceControl != null) {
if (show != mLastSurfaceShowing) {
t.setVisibility(mSurfaceControl, show);
}
}
+ // Only show the overlay if the task has other visible children
+ if (mOverlayHost != null) {
+ mOverlayHost.setVisibility(t, visible);
+ }
mLastSurfaceShowing = show;
}
@@ -3468,6 +3473,54 @@
info.isSleeping = shouldSleepActivities();
}
+ /**
+ * Removes the activity info if the activity belongs to a different uid, which is
+ * different from the app that hosts the task.
+ */
+ static void trimIneffectiveInfo(Task task, TaskInfo info) {
+ final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing,
+ false /* traverseTopToBottom */);
+ final int baseActivityUid =
+ baseActivity != null ? baseActivity.getUid() : task.effectiveUid;
+
+ if (info.topActivityInfo != null
+ && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) {
+ // Making a copy to prevent eliminating the info in the original ActivityRecord.
+ info.topActivityInfo = new ActivityInfo(info.topActivityInfo);
+ info.topActivityInfo.applicationInfo =
+ new ApplicationInfo(info.topActivityInfo.applicationInfo);
+
+ // Strip the sensitive info.
+ info.topActivity = new ComponentName("", "");
+ info.topActivityInfo.packageName = "";
+ info.topActivityInfo.taskAffinity = "";
+ info.topActivityInfo.processName = "";
+ info.topActivityInfo.name = "";
+ info.topActivityInfo.parentActivityName = "";
+ info.topActivityInfo.targetActivity = "";
+ info.topActivityInfo.splitName = "";
+ info.topActivityInfo.applicationInfo.className = "";
+ info.topActivityInfo.applicationInfo.credentialProtectedDataDir = "";
+ info.topActivityInfo.applicationInfo.dataDir = "";
+ info.topActivityInfo.applicationInfo.deviceProtectedDataDir = "";
+ info.topActivityInfo.applicationInfo.manageSpaceActivityName = "";
+ info.topActivityInfo.applicationInfo.nativeLibraryDir = "";
+ info.topActivityInfo.applicationInfo.nativeLibraryRootDir = "";
+ info.topActivityInfo.applicationInfo.processName = "";
+ info.topActivityInfo.applicationInfo.publicSourceDir = "";
+ info.topActivityInfo.applicationInfo.scanPublicSourceDir = "";
+ info.topActivityInfo.applicationInfo.scanSourceDir = "";
+ info.topActivityInfo.applicationInfo.sourceDir = "";
+ info.topActivityInfo.applicationInfo.taskAffinity = "";
+ info.topActivityInfo.applicationInfo.name = "";
+ info.topActivityInfo.applicationInfo.packageName = "";
+ }
+
+ if (task.effectiveUid != baseActivityUid) {
+ info.baseActivity = new ComponentName("", "");
+ }
+ }
+
@Nullable PictureInPictureParams getPictureInPictureParams() {
final Task topTask = getTopMostTask();
if (topTask == null) return null;
@@ -4150,13 +4203,7 @@
@Override
boolean showSurfaceOnCreation() {
- if (mCreatedByOrganizer) {
- // Tasks created by the organizer are default visible because they can synchronously
- // update the leash before new children are added to the task.
- return true;
- }
- // Organized tasks handle their own surface visibility
- return !canBeOrganized();
+ return false;
}
@Override
@@ -6364,6 +6411,11 @@
return this;
}
+ Builder setRemoveWithTaskOrganizer(boolean removeWithTaskOrganizer) {
+ mRemoveWithTaskOrganizer = removeWithTaskOrganizer;
+ return this;
+ }
+
private Builder setUserId(int userId) {
mUserId = userId;
return this;
@@ -6561,7 +6613,7 @@
mCallingPackage = mActivityInfo.packageName;
mResizeMode = mActivityInfo.resizeMode;
mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture();
- if (mActivityOptions != null) {
+ if (!mRemoveWithTaskOrganizer && mActivityOptions != null) {
mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer();
}
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 90a0dff..49b2a4ef 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -540,9 +540,12 @@
synchronized (mGlobalLock) {
final TaskFragmentOrganizerState organizerState =
mTaskFragmentOrganizerState.get(organizer.asBinder());
- return organizerState != null
- ? organizerState.mRemoteAnimationDefinition
- : null;
+ if (organizerState == null) {
+ Slog.e(TAG, "TaskFragmentOrganizer has been unregistered or died when trying"
+ + " to play animation on its organized windows.");
+ return null;
+ }
+ return organizerState.mRemoteAnimationDefinition;
}
}
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index d619547..d780cae 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -783,7 +783,8 @@
}
@Override
- public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) {
+ public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie,
+ boolean removeWithTaskOrganizer) {
enforceTaskPermission("createRootTask()");
final long origId = Binder.clearCallingIdentity();
try {
@@ -795,7 +796,7 @@
return;
}
- createRootTask(display, windowingMode, launchCookie);
+ createRootTask(display, windowingMode, launchCookie, removeWithTaskOrganizer);
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -804,6 +805,12 @@
@VisibleForTesting
Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie) {
+ return createRootTask(display, windowingMode, launchCookie,
+ false /* removeWithTaskOrganizer */);
+ }
+
+ Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie,
+ boolean removeWithTaskOrganizer) {
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Create root task displayId=%d winMode=%d",
display.mDisplayId, windowingMode);
// We want to defer the task appear signal until the task is fully created and attached to
@@ -816,6 +823,7 @@
.setDeferTaskAppear(true)
.setLaunchCookie(launchCookie)
.setParent(display.getDefaultTaskDisplayArea())
+ .setRemoveWithTaskOrganizer(removeWithTaskOrganizer)
.build();
task.setDeferTaskAppear(false /* deferTaskAppear */);
return task;
diff --git a/services/core/java/com/android/server/wm/TrustedOverlayHost.java b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
index 975b21c..88c410b 100644
--- a/services/core/java/com/android/server/wm/TrustedOverlayHost.java
+++ b/services/core/java/com/android/server/wm/TrustedOverlayHost.java
@@ -80,6 +80,12 @@
}
}
+ void setVisibility(SurfaceControl.Transaction t, boolean visible) {
+ if (mSurfaceControl != null) {
+ t.setVisibility(mSurfaceControl, visible);
+ }
+ }
+
void addOverlay(SurfaceControlViewHost.SurfacePackage p, SurfaceControl currentParent) {
requireOverlaySurfaceControl();
mOverlays.add(p);
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 1d25dbc..b2dab78b 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -371,7 +371,7 @@
boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) {
// Size of the display the wallpaper is rendered on.
- final Rect lastWallpaperBounds = wallpaperWin.getLastReportedBounds();
+ final Rect lastWallpaperBounds = wallpaperWin.getParentFrame();
// Full size of the wallpaper (usually larger than bounds above to parallax scroll when
// swiping through Launcher pages).
final Rect wallpaperFrame = wallpaperWin.getFrame();
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index fb584fe..9a20354 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -73,6 +73,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
+import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Point;
@@ -178,8 +179,9 @@
protected final WindowList<E> mChildren = new WindowList<E>();
// The specified orientation for this window container.
- @ActivityInfo.ScreenOrientation
- protected int mOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
+ // Shouldn't be accessed directly since subclasses can override getOverrideOrientation.
+ @ScreenOrientation
+ private int mOverrideOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
/**
* The window container which decides its orientation since the last time
@@ -1427,19 +1429,20 @@
/**
* Gets the configuration orientation by the requested screen orientation
- * ({@link ActivityInfo.ScreenOrientation}) of this activity.
+ * ({@link ScreenOrientation}) of this activity.
*
* @return orientation in ({@link Configuration#ORIENTATION_LANDSCAPE},
* {@link Configuration#ORIENTATION_PORTRAIT},
* {@link Configuration#ORIENTATION_UNDEFINED}).
*/
+ @ScreenOrientation
int getRequestedConfigurationOrientation() {
return getRequestedConfigurationOrientation(false /* forDisplay */);
}
/**
* Gets the configuration orientation by the requested screen orientation
- * ({@link ActivityInfo.ScreenOrientation}) of this activity.
+ * ({@link ScreenOrientation}) of this activity.
*
* @param forDisplay whether it is the requested config orientation for display.
* If {@code true}, we may reverse the requested orientation if the root is
@@ -1450,8 +1453,9 @@
* {@link Configuration#ORIENTATION_PORTRAIT},
* {@link Configuration#ORIENTATION_UNDEFINED}).
*/
+ @ScreenOrientation
int getRequestedConfigurationOrientation(boolean forDisplay) {
- int requestedOrientation = mOrientation;
+ int requestedOrientation = getOverrideOrientation();
final RootDisplayArea root = getRootDisplayArea();
if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) {
// Reverse the requested orientation if the orientation of its root is different from
@@ -1461,7 +1465,7 @@
// (portrait).
// When an app below the DAG is requesting landscape, it should actually request the
// display to be portrait, so that the DAG and the app will be in landscape.
- requestedOrientation = reverseOrientation(mOrientation);
+ requestedOrientation = reverseOrientation(getOverrideOrientation());
}
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
@@ -1486,7 +1490,7 @@
*
* @param orientation the specified orientation.
*/
- void setOrientation(int orientation) {
+ void setOrientation(@ScreenOrientation int orientation) {
setOrientation(orientation, null /* requestingContainer */);
}
@@ -1494,17 +1498,17 @@
* Sets the specified orientation of this container. It percolates this change upward along the
* hierarchy to let each level of the hierarchy a chance to respond to it.
*
- * @param orientation the specified orientation. Needs to be one of {@link
- * android.content.pm.ActivityInfo.ScreenOrientation}.
+ * @param orientation the specified orientation. Needs to be one of {@link ScreenOrientation}.
* @param requestingContainer the container which orientation request has changed. Mostly used
* to ensure it gets correct configuration.
*/
- void setOrientation(int orientation, @Nullable WindowContainer requestingContainer) {
- if (mOrientation == orientation) {
+ void setOrientation(@ScreenOrientation int orientation,
+ @Nullable WindowContainer requestingContainer) {
+ if (getOverrideOrientation() == orientation) {
return;
}
- mOrientation = orientation;
+ setOverrideOrientation(orientation);
final WindowContainer parent = getParent();
if (parent != null) {
if (getConfiguration().orientation != getRequestedConfigurationOrientation()
@@ -1523,9 +1527,9 @@
}
}
- @ActivityInfo.ScreenOrientation
+ @ScreenOrientation
int getOrientation() {
- return getOrientation(mOrientation);
+ return getOrientation(getOverrideOrientation());
}
/**
@@ -1539,7 +1543,8 @@
* better match.
* @return The orientation as specified by this branch or the window hierarchy.
*/
- int getOrientation(int candidate) {
+ @ScreenOrientation
+ int getOrientation(@ScreenOrientation int candidate) {
mLastOrientationSource = null;
if (!providesOrientation()) {
return SCREEN_ORIENTATION_UNSET;
@@ -1549,16 +1554,16 @@
// specified; otherwise we prefer to use the orientation of its topmost child that has one
// specified and fall back on this container's unset or unspecified value as a candidate
// if none of the children have a better candidate for the orientation.
- if (mOrientation != SCREEN_ORIENTATION_UNSET
- && mOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+ if (getOverrideOrientation() != SCREEN_ORIENTATION_UNSET
+ && getOverrideOrientation() != SCREEN_ORIENTATION_UNSPECIFIED) {
mLastOrientationSource = this;
- return mOrientation;
+ return getOverrideOrientation();
}
for (int i = mChildren.size() - 1; i >= 0; --i) {
final WindowContainer wc = mChildren.get(i);
- // TODO: Maybe mOrientation should default to SCREEN_ORIENTATION_UNSET vs.
+ // TODO: Maybe mOverrideOrientation should default to SCREEN_ORIENTATION_UNSET vs.
// SCREEN_ORIENTATION_UNSPECIFIED?
final int orientation = wc.getOrientation(candidate == SCREEN_ORIENTATION_BEHIND
? SCREEN_ORIENTATION_BEHIND : SCREEN_ORIENTATION_UNSET);
@@ -1590,6 +1595,20 @@
}
/**
+ * Returns orientation specified on this level of hierarchy without taking children into
+ * account, like {@link #getOrientation} does, allowing subclasses to override. See {@link
+ * ActivityRecord#getOverrideOrientation} for an example.
+ */
+ @ScreenOrientation
+ protected int getOverrideOrientation() {
+ return mOverrideOrientation;
+ }
+
+ protected void setOverrideOrientation(@ScreenOrientation int orientation) {
+ mOverrideOrientation = orientation;
+ }
+
+ /**
* @return The deepest source which decides the orientation of this window container since the
* last time {@link #getOrientation(int) was called.
*/
@@ -2635,7 +2654,7 @@
final long token = proto.start(fieldId);
super.dumpDebug(proto, CONFIGURATION_CONTAINER, logLevel);
- proto.write(ORIENTATION, mOrientation);
+ proto.write(ORIENTATION, mOverrideOrientation);
proto.write(VISIBLE, isVisible);
writeIdentifierToProto(proto, IDENTIFIER);
if (mSurfaceAnimator.isAnimating()) {
@@ -3197,11 +3216,11 @@
private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter,
boolean isVoiceInteraction) {
- if (isOrganized()
+ if (AppTransitionController.isTaskViewTask(this) || (isOrganized()
// TODO(b/161711458): Clean-up when moved to shell.
&& getWindowingMode() != WINDOWING_MODE_FULLSCREEN
&& getWindowingMode() != WINDOWING_MODE_FREEFORM
- && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) {
+ && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) {
return null;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4e32a7c..2911890 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -167,7 +167,6 @@
import android.app.IAssistDataReceiver;
import android.app.WindowConfiguration;
import android.content.BroadcastReceiver;
-import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -177,7 +176,6 @@
import android.content.pm.PackageManagerInternal;
import android.content.pm.TestUtilityService;
import android.content.res.Configuration;
-import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.Bitmap;
@@ -4172,7 +4170,8 @@
* <p>Note: this assumes that {@link #mGlobalLock} is held by the caller.
*/
boolean isIgnoreOrientationRequestDisabled() {
- return mIsIgnoreOrientationRequestDisabled;
+ return mIsIgnoreOrientationRequestDisabled
+ || !mLetterboxConfiguration.isIgnoreOrientationRequestAllowed();
}
@Override
@@ -5839,6 +5838,11 @@
if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) {
return displayContent.mInitialDisplayDensity;
}
+
+ DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+ if (info != null && info.hasAccess(Binder.getCallingUid())) {
+ return info.logicalDensityDpi;
+ }
}
return -1;
}
@@ -5870,6 +5874,11 @@
final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
if (displayContent != null) {
displayContent.setForcedDensity(density, targetUserId);
+ } else {
+ DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+ if (info != null) {
+ mDisplayWindowSettings.setForcedDensity(info, density, userId);
+ }
}
}
} finally {
@@ -5894,6 +5903,12 @@
if (displayContent != null) {
displayContent.setForcedDensity(displayContent.mInitialDisplayDensity,
callingUserId);
+ } else {
+ DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+ if (info != null) {
+ mDisplayWindowSettings.setForcedDensity(info, info.logicalDensityDpi,
+ userId);
+ }
}
}
} finally {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index aef6d1d..a06d84c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -955,6 +955,10 @@
runSetBooleanFlag(pw, mLetterboxConfiguration
::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
break;
+ case "--isDisplayAspectRatioEnabledForFixedOrientationLetterbox":
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox);
+ break;
case "--isTranslucentLetterboxingEnabled":
runSetBooleanFlag(pw, mLetterboxConfiguration
::setTranslucentLetterboxingOverrideEnabled);
@@ -1030,6 +1034,10 @@
mLetterboxConfiguration
.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
break;
+ case "IsDisplayAspectRatioEnabledForFixedOrientationLetterbox":
+ mLetterboxConfiguration
+ .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
+ break;
case "isTranslucentLetterboxingEnabled":
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
break;
@@ -1140,6 +1148,7 @@
mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
mLetterboxConfiguration.resetIsEducationEnabled();
mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
@@ -1187,7 +1196,9 @@
pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
+ mLetterboxConfiguration
.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
-
+ pw.println("Is using display aspect ratio as aspect ratio for all letterboxed apps: "
+ + mLetterboxConfiguration
+ .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
pw.println(" Is activity \"refresh\" in camera compatibility treatment enabled: "
+ mLetterboxConfiguration.isCameraCompatRefreshEnabled());
pw.println(" Refresh using \"stopped -> resumed\" cycle: "
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3187337..fd47753 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1949,12 +1949,21 @@
creationParams.getPairedPrimaryFragmentToken());
final int pairedPosition = ownerTask.mChildren.indexOf(pairedPrimaryTaskFragment);
position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP;
+ } else if (creationParams.getPairedActivityToken() != null) {
+ // When there is a paired Activity, we want to place the new TaskFragment right above
+ // the paired Activity to make sure the Activity position is not changed after reparent.
+ final ActivityRecord pairedActivity = ActivityRecord.forTokenLocked(
+ creationParams.getPairedActivityToken());
+ final int pairedPosition = ownerTask.mChildren.indexOf(pairedActivity);
+ position = pairedPosition != -1 ? pairedPosition + 1 : POSITION_TOP;
} else {
position = POSITION_TOP;
}
ownerTask.addChild(taskFragment, position);
taskFragment.setWindowingMode(creationParams.getWindowingMode());
taskFragment.setBounds(creationParams.getInitialBounds());
+ // Record the initial relative embedded bounds.
+ taskFragment.updateRelativeEmbeddedBounds();
mLaunchTaskFragments.put(creationParams.getFragmentToken(), taskFragment);
if (transition != null) transition.collectExistenceChange(taskFragment);
diff --git a/services/core/java/com/android/server/wm/WindowOrientationListener.java b/services/core/java/com/android/server/wm/WindowOrientationListener.java
index 3e165e4..14c816d 100644
--- a/services/core/java/com/android/server/wm/WindowOrientationListener.java
+++ b/services/core/java/com/android/server/wm/WindowOrientationListener.java
@@ -88,14 +88,19 @@
private final Object mLock = new Object();
+ @Surface.Rotation
+ private final int mDefaultRotation;
+
/**
* Creates a new WindowOrientationListener.
*
* @param context for the WindowOrientationListener.
* @param handler Provides the Looper for receiving sensor updates.
+ * @param defaultRotation Default rotation of the display.
*/
- public WindowOrientationListener(Context context, Handler handler) {
- this(context, handler, SensorManager.SENSOR_DELAY_UI);
+ public WindowOrientationListener(Context context, Handler handler,
+ @Surface.Rotation int defaultRotation) {
+ this(context, handler, defaultRotation, SensorManager.SENSOR_DELAY_UI);
}
/**
@@ -103,7 +108,7 @@
*
* @param context for the WindowOrientationListener.
* @param handler Provides the Looper for receiving sensor updates.
- * @param wmService WindowManagerService to read the device config from.
+ * @param defaultRotation Default rotation of the display.
* @param rate at which sensor events are processed (see also
* {@link android.hardware.SensorManager SensorManager}). Use the default
* value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
@@ -111,10 +116,11 @@
*
* This constructor is private since no one uses it.
*/
- private WindowOrientationListener(
- Context context, Handler handler, int rate) {
+ private WindowOrientationListener(Context context, Handler handler,
+ @Surface.Rotation int defaultRotation, int rate) {
mContext = context;
mHandler = handler;
+ mDefaultRotation = defaultRotation;
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
mRate = rate;
List<Sensor> l = mSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION);
@@ -1159,7 +1165,7 @@
"Reusing the last rotation resolution: " + mLastRotationResolution);
finalizeRotation(mLastRotationResolution);
} else {
- finalizeRotation(Surface.ROTATION_0);
+ finalizeRotation(mDefaultRotation);
}
return;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0469961..45dacbb 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -396,14 +396,6 @@
int mPrepareSyncSeqId = 0;
/**
- * {@code true} when the client was still drawing for sync when the sync-set was finished or
- * cancelled. This can happen if the window goes away during a sync. In this situation we need
- * to make sure to still apply the postDrawTransaction when it finishes to prevent the client
- * from getting stuck in a bad state.
- */
- boolean mClientWasDrawingForSync = false;
-
- /**
* Special mode that is intended only for the rounded corner overlay: during rotation
* transition, we un-rotate the window token such that the window appears as it did before the
* rotation.
@@ -2395,7 +2387,11 @@
// IME parent may failed to attach to the app during rotating the screen.
// See DisplayContent#shouldImeAttachedToApp, DisplayContent#isImeControlledByApp
if (windowConfigChanged) {
- getDisplayContent().updateImeControlTarget();
+ // If the window was the IME layering target, updates the IME surface parent in case
+ // the IME surface may be wrongly positioned when the window configuration affects the
+ // IME surface association. (e.g. Attach IME surface on the display instead of the
+ // app when the app bounds being letterboxed.)
+ mDisplayContent.updateImeControlTarget(isImeLayeringTarget() /* updateImeParent */);
}
}
@@ -3081,12 +3077,6 @@
return mLastReportedConfiguration.getMergedConfiguration();
}
- /** Returns the last window configuration bounds reported to the client. */
- Rect getLastReportedBounds() {
- final Rect bounds = getLastReportedConfiguration().windowConfiguration.getBounds();
- return !bounds.isEmpty() ? bounds : getBounds();
- }
-
void adjustStartingWindowFlags() {
if (mAttrs.type == TYPE_BASE_APPLICATION && mActivityRecord != null
&& mActivityRecord.mStartingWindow != null) {
@@ -4421,6 +4411,9 @@
pw.print("null");
}
+ if (mXOffset != 0 || mYOffset != 0) {
+ pw.println(prefix + "mXOffset=" + mXOffset + " mYOffset=" + mYOffset);
+ }
if (mHScale != 1 || mVScale != 1) {
pw.println(prefix + "mHScale=" + mHScale
+ " mVScale=" + mVScale);
@@ -5573,7 +5566,7 @@
mSurfacePosition);
if (mWallpaperScale != 1f) {
- final Rect bounds = getLastReportedBounds();
+ final Rect bounds = getParentFrame();
Matrix matrix = mTmpMatrix;
matrix.setTranslate(mXOffset, mYOffset);
matrix.postScale(mWallpaperScale, mWallpaperScale, bounds.exactCenterX(),
@@ -5686,6 +5679,14 @@
&& imeTarget.compareTo(this) <= 0;
return inTokenWithAndAboveImeTarget;
}
+
+ // The condition is for the system dialog not belonging to any Activity.
+ // (^FLAG_NOT_FOCUSABLE & FLAG_ALT_FOCUSABLE_IM) means the dialog is still focusable but
+ // should be placed above the IME window.
+ if ((mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM))
+ == FLAG_ALT_FOCUSABLE_IM && isTrustedOverlay() && canAddInternalSystemWindow()) {
+ return true;
+ }
return false;
}
@@ -6019,9 +6020,6 @@
@Override
void finishSync(Transaction outMergedTransaction, boolean cancel) {
- if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) {
- mClientWasDrawingForSync = true;
- }
mPrepareSyncSeqId = 0;
if (cancel) {
// This is leaving sync so any buffers left in the sync have a chance of
@@ -6089,9 +6087,7 @@
layoutNeeded = onSyncFinishedDrawing();
}
- layoutNeeded |=
- mWinAnimator.finishDrawingLocked(postDrawTransaction, mClientWasDrawingForSync);
- mClientWasDrawingForSync = false;
+ layoutNeeded |= mWinAnimator.finishDrawingLocked(postDrawTransaction);
// We always want to force a traversal after a finish draw for blast sync.
return !skipLayout && (hasSyncHandlers || layoutNeeded);
}
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index a0ba8fd..f364248 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -151,17 +151,6 @@
int mAttrType;
- /**
- * Handles surface changes synchronized to after the client has drawn the surface. This
- * transaction is currently used to reparent the old surface children to the new surface once
- * the client has completed drawing to the new surface.
- * This transaction is also used to merge transactions parceled in by the client. The client
- * uses the transaction to update the relative z of its children from the old parent surface
- * to the new parent surface once window manager reparents its children.
- */
- private final SurfaceControl.Transaction mPostDrawTransaction =
- new SurfaceControl.Transaction();
-
WindowStateAnimator(final WindowState win) {
final WindowManagerService service = win.mWmService;
@@ -217,8 +206,7 @@
}
}
- boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction,
- boolean forceApplyNow) {
+ boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction) {
final boolean startingWindow =
mWin.mAttrs.type == WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
if (startingWindow) {
@@ -240,14 +228,7 @@
}
if (postDrawTransaction != null) {
- // If there is no surface, the last draw was for the previous surface. We don't want to
- // wait until the new surface is shown and instead just apply the transaction right
- // away.
- if (mLastHidden && mDrawState != NO_SURFACE && !forceApplyNow) {
- mPostDrawTransaction.merge(postDrawTransaction);
- } else {
- mWin.getSyncTransaction().merge(postDrawTransaction);
- }
+ mWin.getSyncTransaction().merge(postDrawTransaction);
layoutNeeded = true;
}
@@ -547,7 +528,6 @@
if (!shown)
return false;
- t.merge(mPostDrawTransaction);
return true;
}
@@ -714,10 +694,6 @@
}
void destroySurface(SurfaceControl.Transaction t) {
- // Since the SurfaceControl is getting torn down, it's safe to just clean up any
- // pending transactions that were in mPostDrawTransaction, as well.
- t.merge(mPostDrawTransaction);
-
try {
if (mSurfaceController != null) {
mSurfaceController.destroy(t);
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index f628fba..abe48f8 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -464,6 +464,14 @@
</xs:complexType>
<xs:complexType name="refreshRateConfigs">
+ <xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger"
+ minOccurs="0" maxOccurs="1">
+ <xs:annotation name="final"/>
+ </xs:element>
+ <xs:element name="defaultPeakRefreshRate" type="xs:nonNegativeInteger"
+ minOccurs="0" maxOccurs="1">
+ <xs:annotation name="final"/>
+ </xs:element>
<xs:element name="lowerBlockingZoneConfigs" type="blockingZoneConfig"
minOccurs="0" maxOccurs="1">
<xs:annotation name="final"/>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index cb08179..2c97af5 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -186,8 +186,12 @@
public class RefreshRateConfigs {
ctor public RefreshRateConfigs();
+ method public final java.math.BigInteger getDefaultPeakRefreshRate();
+ method public final java.math.BigInteger getDefaultRefreshRate();
method public final com.android.server.display.config.BlockingZoneConfig getHigherBlockingZoneConfigs();
method public final com.android.server.display.config.BlockingZoneConfig getLowerBlockingZoneConfigs();
+ method public final void setDefaultPeakRefreshRate(java.math.BigInteger);
+ method public final void setDefaultRefreshRate(java.math.BigInteger);
method public final void setHigherBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
method public final void setLowerBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
}
diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
index 8a3a44ae..0993295 100644
--- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
+++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
@@ -21,6 +21,7 @@
import android.annotation.WorkerThread;
import android.content.Context;
import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
@@ -149,6 +150,8 @@
found = true;
}
+ } catch (SQLiteException exception) {
+ Slog.w("SQLite exception when querying contacts.", exception);
}
if (found && lookupKey != null && hasPhoneNumber) {
return queryPhoneNumber(lookupKey);
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index 693f3a0..eff9e8d 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -271,22 +271,22 @@
private ConversationChannel getConversationChannel(String packageName, int userId,
String shortcutId, ConversationInfo conversationInfo) {
ShortcutInfo shortcutInfo = getShortcut(packageName, userId, shortcutId);
- return getConversationChannel(shortcutInfo, conversationInfo);
+ return getConversationChannel(
+ shortcutInfo, conversationInfo, packageName, userId, shortcutId);
}
@Nullable
private ConversationChannel getConversationChannel(ShortcutInfo shortcutInfo,
- ConversationInfo conversationInfo) {
+ ConversationInfo conversationInfo, String packageName, int userId, String shortcutId) {
if (conversationInfo == null || conversationInfo.isDemoted()) {
return null;
}
if (shortcutInfo == null) {
- Slog.e(TAG, " Shortcut no longer found");
+ Slog.e(TAG, "Shortcut no longer found");
+ mInjector.getBackgroundExecutor().execute(
+ () -> removeConversations(packageName, userId, Set.of(shortcutId)));
return null;
}
- String packageName = shortcutInfo.getPackage();
- String shortcutId = shortcutInfo.getId();
- int userId = shortcutInfo.getUserId();
int uid = mPackageManagerInternal.getPackageUid(packageName, 0, userId);
NotificationChannel parentChannel =
mNotificationManagerInternal.getNotificationChannel(packageName, uid,
@@ -1130,38 +1130,41 @@
public void onShortcutsRemoved(@NonNull String packageName,
@NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
mInjector.getBackgroundExecutor().execute(() -> {
- int uid = Process.INVALID_UID;
- try {
- uid = mContext.getPackageManager().getPackageUidAsUser(
- packageName, user.getIdentifier());
- } catch (PackageManager.NameNotFoundException e) {
- Slog.e(TAG, "Package not found: " + packageName, e);
- }
- PackageData packageData = getPackage(packageName, user.getIdentifier());
- Set<String> shortcutIds = new HashSet<>();
+ HashSet<String> shortcutIds = new HashSet<>();
for (ShortcutInfo shortcutInfo : shortcuts) {
- if (packageData != null) {
- if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutInfo.getId());
- packageData.deleteDataForConversation(shortcutInfo.getId());
- }
shortcutIds.add(shortcutInfo.getId());
}
- if (uid != Process.INVALID_UID) {
- mNotificationManagerInternal.onConversationRemoved(
- packageName, uid, shortcutIds);
- }
+ removeConversations(packageName, user.getIdentifier(), shortcutIds);
});
}
}
+ private void removeConversations(
+ @NonNull String packageName, @NonNull int userId, @NonNull Set<String> shortcutIds) {
+ PackageData packageData = getPackage(packageName, userId);
+ if (packageData != null) {
+ for (String shortcutId : shortcutIds) {
+ if (DEBUG) Log.d(TAG, "Deleting shortcut: " + shortcutId);
+ packageData.deleteDataForConversation(shortcutId);
+ }
+ }
+ try {
+ int uid = mContext.getPackageManager().getPackageUidAsUser(
+ packageName, userId);
+ mNotificationManagerInternal.onConversationRemoved(packageName, uid, shortcutIds);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Package not found when removing conversation: " + packageName, e);
+ }
+ }
+
/** Listener for the notifications and their settings changes. */
private class NotificationListener extends NotificationListenerService {
private final int mUserId;
- // Conversation package name + shortcut ID -> Number of active notifications
+ // Conversation package name + shortcut ID -> Keys of active notifications
@GuardedBy("this")
- private final Map<Pair<String, String>, Integer> mActiveNotifCounts = new ArrayMap<>();
+ private final Map<Pair<String, String>, Set<String>> mActiveNotifKeys = new ArrayMap<>();
private NotificationListener(int userId) {
mUserId = userId;
@@ -1175,8 +1178,10 @@
String shortcutId = sbn.getNotification().getShortcutId();
PackageData packageData = getPackageIfConversationExists(sbn, conversationInfo -> {
synchronized (this) {
- mActiveNotifCounts.merge(
- Pair.create(sbn.getPackageName(), shortcutId), 1, Integer::sum);
+ Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent(
+ Pair.create(sbn.getPackageName(), shortcutId),
+ (unusedKey) -> new HashSet<>());
+ notificationKeys.add(sbn.getKey());
}
});
@@ -1215,12 +1220,12 @@
Pair<String, String> conversationKey =
Pair.create(sbn.getPackageName(), shortcutId);
synchronized (this) {
- int count = mActiveNotifCounts.getOrDefault(conversationKey, 0) - 1;
- if (count <= 0) {
- mActiveNotifCounts.remove(conversationKey);
+ Set<String> notificationKeys = mActiveNotifKeys.computeIfAbsent(
+ conversationKey, (unusedKey) -> new HashSet<>());
+ notificationKeys.remove(sbn.getKey());
+ if (notificationKeys.isEmpty()) {
+ mActiveNotifKeys.remove(conversationKey);
cleanupCachedShortcuts(mUserId, MAX_CACHED_RECENT_SHORTCUTS);
- } else {
- mActiveNotifCounts.put(conversationKey, count);
}
}
});
@@ -1286,7 +1291,7 @@
}
synchronized boolean hasActiveNotifications(String packageName, String shortcutId) {
- return mActiveNotifCounts.containsKey(Pair.create(packageName, shortcutId));
+ return mActiveNotifKeys.containsKey(Pair.create(packageName, shortcutId));
}
}
@@ -1349,9 +1354,11 @@
}
private void updateConversationStoreThenNotifyListeners(ConversationStore cs,
- ConversationInfo modifiedConv, ShortcutInfo shortcutInfo) {
+ ConversationInfo modifiedConv, @NonNull ShortcutInfo shortcutInfo) {
cs.addOrUpdate(modifiedConv);
- ConversationChannel channel = getConversationChannel(shortcutInfo, modifiedConv);
+ ConversationChannel channel = getConversationChannel(
+ shortcutInfo, modifiedConv, shortcutInfo.getPackage(), shortcutInfo.getUserId(),
+ shortcutInfo.getId());
if (channel != null) {
notifyConversationsListeners(Arrays.asList(channel));
}
diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml
index 33ac735..ea0481e 100644
--- a/services/tests/mockingservicestests/AndroidManifest.xml
+++ b/services/tests/mockingservicestests/AndroidManifest.xml
@@ -43,6 +43,9 @@
<!-- needed by TrustManagerServiceTest to access LockSettings' secure storage -->
<uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" />
+ <!-- needed by GameManagerServiceTest because GameManager creates a UidObserver -->
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+
<application android:testOnly="true"
android:debuggable="true">
<uses-library android:name="android.test.runner" />
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index fa4a9de..2d5f0b0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -29,13 +29,16 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.Manifest;
+import android.app.ActivityManager;
import android.app.GameManager;
import android.app.GameModeInfo;
import android.app.GameState;
@@ -203,6 +206,23 @@
LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
}
+ private void mockAppCategory(String packageName, @ApplicationInfo.Category int category)
+ throws Exception {
+ reset(mMockPackageManager);
+ final ApplicationInfo gameApplicationInfo = new ApplicationInfo();
+ gameApplicationInfo.category = category;
+ gameApplicationInfo.packageName = packageName;
+ final PackageInfo pi = new PackageInfo();
+ pi.packageName = packageName;
+ pi.applicationInfo = gameApplicationInfo;
+ final List<PackageInfo> packages = new ArrayList<>();
+ packages.add(pi);
+ when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
+ .thenReturn(packages);
+ when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenReturn(gameApplicationInfo);
+ }
+
@After
public void tearDown() throws Exception {
LocalServices.removeServiceForTest(PowerManagerInternal.class);
@@ -1597,4 +1617,113 @@
ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
ArgumentMatchers.eq(0.0f));
}
+
+ private GameManagerService createServiceAndStartUser(int userId) {
+ GameManagerService gameManagerService = new GameManagerService(mMockContext,
+ mTestLooper.getLooper());
+ startUser(gameManagerService, userId);
+ return gameManagerService;
+ }
+
+ @Test
+ public void testGamePowerMode_gamePackage() throws Exception {
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages = {mPackageName};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+ }
+
+ @Test
+ public void testGamePowerMode_twoGames() throws Exception {
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages1 = {mPackageName};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
+ String someGamePkg = "some.game";
+ String[] packages2 = {someGamePkg};
+ int somePackageId = DEFAULT_PACKAGE_UID + 1;
+ when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+ HashMap<Integer, Boolean> powerState = new HashMap<>();
+ doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1)))
+ .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean());
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ assertTrue(powerState.get(Mode.GAME));
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ assertTrue(powerState.get(Mode.GAME));
+ gameManagerService.mUidObserver.onUidStateChanged(
+ somePackageId, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ assertFalse(powerState.get(Mode.GAME));
+ }
+
+ @Test
+ public void testGamePowerMode_twoGamesOverlap() throws Exception {
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages1 = {mPackageName};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages1);
+ String someGamePkg = "some.game";
+ String[] packages2 = {someGamePkg};
+ int somePackageId = DEFAULT_PACKAGE_UID + 1;
+ when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ somePackageId, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);
+ verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+ }
+
+ @Test
+ public void testGamePowerMode_released() throws Exception {
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages = {mPackageName};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);
+ }
+
+ @Test
+ public void testGamePowerMode_noPackage() throws Exception {
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages = {};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+ }
+
+ @Test
+ public void testGamePowerMode_notAGamePackage() throws Exception {
+ mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages = {"someapp"};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, true);
+ }
+
+ @Test
+ public void testGamePowerMode_notAGamePackageNotReleased() throws Exception {
+ mockAppCategory(mPackageName, ApplicationInfo.CATEGORY_IMAGE);
+ GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+ String[] packages = {"someapp"};
+ when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);
+ gameManagerService.mUidObserver.onUidStateChanged(
+ DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ verify(mMockPowerManager, times(0)).setPowerMode(Mode.GAME, false);
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
index e4f9eaf..9b23f8b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameServiceProviderInstanceImplTest.java
@@ -1089,8 +1089,7 @@
Consumer<Uri> consumer = invocation.getArgument(invocation.getArguments().length - 1);
consumer.accept(Uri.parse("a/b.png"));
return null;
- }).when(mMockScreenshotHelper).provideScreenshot(
- any(), any(), any(), anyInt(), anyInt(), any(), anyInt(), any(), any());
+ }).when(mMockScreenshotHelper).takeScreenshot(any(), any(), any());
mGameServiceProviderInstance.start();
startTask(taskId, GAME_A_MAIN_ACTIVITY);
mockPermissionGranted(Manifest.permission.MANAGE_GAME_ACTIVITY);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index c15f6a9..5792ecb 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -17,7 +17,6 @@
package com.android.server.accessibility;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
-import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
@@ -301,9 +300,7 @@
mSystemActionPerformer.performSystemAction(
AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
verify(mMockScreenshotHelper).takeScreenshot(
- eq(TAKE_SCREENSHOT_FULLSCREEN),
- eq(SCREENSHOT_ACCESSIBILITY_ACTIONS),
- any(Handler.class), any());
+ eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any());
}
// PendingIntent is a final class and cannot be mocked. So we are using this
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index dad9fe8..31599ee 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -74,7 +74,7 @@
mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
mSpySystemServer = spy(new NoOpSystemServerAdapter());
mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory,
- mSpySystemServer);
+ mSpySystemServer, mSpyAudioSystem);
mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 666d401..3c735e3 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -41,7 +41,6 @@
import android.hardware.biometrics.fingerprint.ISession;
import android.hardware.biometrics.fingerprint.PointerContext;
import android.hardware.fingerprint.Fingerprint;
-import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.ISidefpsController;
import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -55,7 +54,6 @@
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.internal.R;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.log.CallbackWithProbe;
@@ -369,274 +367,6 @@
verify(mCancellationSignal).cancel();
}
- @Test
- public void fingerprintPowerIgnoresAuthInWindow() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- client.onPowerPressed();
- client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, 2 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
-
- verify(mCallback).onClientFinished(any(), eq(false));
- verify(mCancellationSignal).cancel();
- }
-
- @Test
- public void fingerprintAuthIgnoredWaitingForPower() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- client.onAuthenticated(new Fingerprint("friendly", 3 /* fingerId */, 4 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- client.onPowerPressed();
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
-
- verify(mCallback).onClientFinished(any(), eq(false));
- verify(mCancellationSignal).cancel();
- }
-
- @Test
- public void fingerprintAuthFailsWhenAuthAfterPower() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- client.onPowerPressed();
- mLooper.dispatchAll();
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
-
- verify(mCallback, never()).onClientFinished(any(), eq(true));
- verify(mCallback).onClientFinished(any(), eq(false));
- when(mHal.authenticateWithContext(anyLong(), any())).thenReturn(mCancellationSignal);
- }
-
- @Test
- public void sideFingerprintDoesntSendAuthImmediately() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- mLooper.dispatchAll();
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
-
- verify(mCallback, never()).onClientFinished(any(), anyBoolean());
- }
-
- @Test
- public void sideFingerprintSkipsWindowIfFingerUp() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- mLooper.dispatchAll();
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- client.onAcquired(FINGER_UP, 0);
- mLooper.dispatchAll();
-
- verify(mCallback).onClientFinished(any(), eq(true));
- }
-
- @Test
- public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- final int vendorAcquireMessage = 1234;
-
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
- FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
- vendorAcquireMessage);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- mLooper.dispatchAll();
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage);
- mLooper.dispatchAll();
-
- verify(mCallback).onClientFinished(any(), eq(true));
- }
-
- @Test
- public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- final int vendorAcquireMessage = 1234;
-
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
- FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
- vendorAcquireMessage);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- mLooper.dispatchAll();
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1);
- mLooper.dispatchAll();
-
- verify(mCallback, never()).onClientFinished(any(), anyBoolean());
- }
-
- @Test
- public void sideFingerprintSendsAuthIfFingerUp() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- mLooper.dispatchAll();
- client.onAcquired(FINGER_UP, 0);
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
-
- verify(mCallback).onClientFinished(any(), eq(true));
- }
-
- @Test
- public void sideFingerprintShortCircuitExpires() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
- final int timeBeforeAuthSent = 500;
-
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsKeyguardPowerPressWindow, timeBeforeAuthSent);
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- mLooper.dispatchAll();
- client.onAcquired(FINGER_UP, 0);
- mLooper.dispatchAll();
-
- mLooper.moveTimeForward(500);
- mLooper.dispatchAll();
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
- verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
- mLooper.moveTimeForward(500);
- mLooper.dispatchAll();
- verify(mCallback).onClientFinished(any(), eq(true));
- }
-
- @Test
- public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception {
- final int powerWindow = 500;
- final long authStart = 300;
-
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
-
- // Acquire start occurs at time = 0ms
- when(mClock.millis()).thenReturn(0L);
- client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
-
- // Auth occurs at time = 300
- when(mClock.millis()).thenReturn(authStart);
- // At this point the delay should be 500 - (300 - 0) == 200 milliseconds.
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
- verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
- // After waiting 200 milliseconds, auth should succeed.
- mLooper.moveTimeForward(powerWindow - authStart);
- mLooper.dispatchAll();
- verify(mCallback).onClientFinished(any(), eq(true));
- }
-
- @Test
- public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception {
- final int powerWindow = 500;
-
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
- mContext.getOrCreateTestableResources().addOverride(
- R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
- // Acquire start occurs at time = 0ms
- when(mClock.millis()).thenReturn(0L);
- client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
-
- // Auth reject occurs at time = 300ms
- when(mClock.millis()).thenReturn(300L);
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- false /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
-
- mLooper.moveTimeForward(300);
- mLooper.dispatchAll();
- verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
- when(mClock.millis()).thenReturn(1300L);
- client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
-
- // If code is correct, the new acquired start timestamp should be used
- // and the code should only have to wait 500 - (1500-1300)ms.
- when(mClock.millis()).thenReturn(1500L);
- client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
- true /* authenticated */, new ArrayList<>());
- mLooper.dispatchAll();
-
- mLooper.moveTimeForward(299);
- mLooper.dispatchAll();
- verify(mCallback, never()).onClientFinished(any(), anyBoolean());
-
- mLooper.moveTimeForward(1);
- mLooper.dispatchAll();
- verify(mCallback).onClientFinished(any(), eq(true));
- }
-
- @Test
- public void sideFpsPowerPressCancelsIsntantly() throws Exception {
- when(mSensorProps.isAnySidefpsType()).thenReturn(true);
-
- final FingerprintAuthenticationClient client = createClient(1);
- client.start(mCallback);
-
- client.onPowerPressed();
- mLooper.dispatchAll();
-
- verify(mCallback, never()).onClientFinished(any(), eq(true));
- verify(mCallback).onClientFinished(any(), eq(false));
- }
-
private FingerprintAuthenticationClient createClient() throws RemoteException {
return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 86c5937..77e5d1d 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -51,6 +51,8 @@
public final class DisplayDeviceConfigTest {
private static final int DEFAULT_PEAK_REFRESH_RATE = 75;
private static final int DEFAULT_REFRESH_RATE = 120;
+ private static final int DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE = 55;
+ private static final int DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE = 95;
private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30};
private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21};
private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160};
@@ -150,8 +152,10 @@
assertEquals("ProximitySensor123", mDisplayDeviceConfig.getProximitySensor().name);
assertEquals("prox_type_1", mDisplayDeviceConfig.getProximitySensor().type);
- assertEquals(75, mDisplayDeviceConfig.getDefaultLowRefreshRate());
- assertEquals(90, mDisplayDeviceConfig.getDefaultHighRefreshRate());
+ assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate());
+ assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate());
+ assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate());
+ assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate());
assertArrayEquals(new int[]{45, 55},
mDisplayDeviceConfig.getLowDisplayBrightnessThresholds());
assertArrayEquals(new int[]{50, 60},
@@ -230,8 +234,12 @@
mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA);
assertArrayEquals(new float[]{29, 30, 31},
mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA);
- assertEquals(mDisplayDeviceConfig.getDefaultLowRefreshRate(), DEFAULT_REFRESH_RATE);
- assertEquals(mDisplayDeviceConfig.getDefaultHighRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(),
+ DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate(),
+ DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
+ assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE);
assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(),
LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(),
@@ -449,6 +457,8 @@
+ "<type>prox_type_1</type>\n"
+ "</proxSensor>\n"
+ "<refreshRate>\n"
+ + "<defaultRefreshRate>45</defaultRefreshRate>\n"
+ + "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n"
+ "<lowerBlockingZoneConfigs>\n"
+ "<defaultRefreshRate>75</defaultRefreshRate>\n"
+ "<blockingZoneThreshold>\n"
@@ -550,10 +560,14 @@
.thenReturn(new int[]{370, 380, 390});
// Configs related to refresh rates and blocking zones
- when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate))
+ when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate))
.thenReturn(DEFAULT_PEAK_REFRESH_RATE);
- when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate))
+ when(mResources.getInteger(R.integer.config_defaultRefreshRate))
.thenReturn(DEFAULT_REFRESH_RATE);
+ when(mResources.getInteger(R.integer.config_fixedRefreshRateInHighZone))
+ .thenReturn(DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
+ when(mResources.getInteger(R.integer.config_defaultRefreshRateInZone))
+ .thenReturn(DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE);
when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
.thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 2edb909..52fade1 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -424,6 +424,37 @@
}
/**
+ * Tests that HighBrightnessModeMetadata is non-null on all display devices.
+ */
+ @Test
+ public void testHighBrightnessModeMetadataNonNull() throws Exception {
+ DisplayManagerService displayManager =
+ new DisplayManagerService(mContext, mShortMockedInjector);
+ registerDefaultDisplays(displayManager);
+ displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+ // Add the FakeDisplayDevice
+ FakeDisplayDevice displayDevice = new FakeDisplayDevice("unique_hbm_device");
+ DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+
+ displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+
+ LogicalDisplay logicalDisplay = new LogicalDisplay(1, 1, displayDevice);
+ HighBrightnessModeMetadata hbmMeta =
+ displayManager.getHighBrightnessModeMetadata(logicalDisplay);
+
+ assertNotNull(hbmMeta);
+
+ // Check is Hbm metadata is correctly added for the display device.
+ String uniqueId = displayDevice.getUniqueId();
+ assertTrue(uniqueId.equals("unique_hbm_device"));
+ assertTrue(displayManager.mHighBrightnessModeMetadataMap.containsKey(uniqueId));
+ HighBrightnessModeMetadata hbmMetaFromMap =
+ displayManager.mHighBrightnessModeMetadataMap.get(uniqueId);
+ assertEquals(hbmMeta, hbmMetaFromMap);
+ }
+
+ /**
* Tests that we get a Runtime exception when we cannot initialize the default display.
*/
@Test
@@ -1349,6 +1380,11 @@
super(null, null, "", mContext);
}
+ FakeDisplayDevice(String uniqueDeviceId) {
+ super(null, null, uniqueDeviceId, mContext);
+ }
+
+
public void setDisplayDeviceInfo(DisplayDeviceInfo displayDeviceInfo) {
mDisplayDeviceInfo = displayDeviceInfo;
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index b133a2a..af39dd4 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -1869,6 +1869,10 @@
.thenReturn(75);
when(resources.getInteger(R.integer.config_defaultRefreshRate))
.thenReturn(45);
+ when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone))
+ .thenReturn(65);
+ when(resources.getInteger(R.integer.config_defaultRefreshRateInZone))
+ .thenReturn(85);
when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
.thenReturn(new int[]{5});
when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
@@ -1888,6 +1892,8 @@
assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 45, 0.0);
assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 75,
0.0);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 85);
assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
new int[]{250});
assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -1899,17 +1905,21 @@
// Notify that the default display is updated, such that DisplayDeviceConfig has new values
DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class);
- when(displayDeviceConfig.getDefaultLowRefreshRate()).thenReturn(50);
- when(displayDeviceConfig.getDefaultHighRefreshRate()).thenReturn(55);
+ when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
+ when(displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
+ when(displayDeviceConfig.getDefaultRefreshRate()).thenReturn(60);
+ when(displayDeviceConfig.getDefaultPeakRefreshRate()).thenReturn(65);
when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100});
director.defaultDisplayDeviceUpdated(displayDeviceConfig);
- assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0);
- assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 55,
+ assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
+ assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 65,
0.0);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 55);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 50);
assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
new int[]{210});
assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -1922,6 +1932,8 @@
// Notify that the default display is updated, such that DeviceConfig has new values
FakeDeviceConfig config = mInjector.getDeviceConfig();
config.setDefaultPeakRefreshRate(60);
+ config.setRefreshRateInHighZone(65);
+ config.setRefreshRateInLowZone(70);
config.setLowAmbientBrightnessThresholds(new int[]{20});
config.setLowDisplayBrightnessThresholds(new int[]{10});
config.setHighDisplayBrightnessThresholds(new int[]{255});
@@ -1929,9 +1941,11 @@
director.defaultDisplayDeviceUpdated(displayDeviceConfig);
- assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0);
+ assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 60,
0.0);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65);
+ assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 70);
assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
new int[]{255});
assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -1971,8 +1985,8 @@
any(Handler.class));
DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class);
- when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50);
- when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55);
+ when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
+ when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55);
when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
diff --git a/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java
new file mode 100644
index 0000000..24fc348
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertEquals;
+
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class HbmEventTest {
+ private long mStartTimeMillis;
+ private long mEndTimeMillis;
+ private HbmEvent mHbmEvent;
+
+ @Before
+ public void setUp() {
+ mStartTimeMillis = 10;
+ mEndTimeMillis = 20;
+ mHbmEvent = new HbmEvent(mStartTimeMillis, mEndTimeMillis);
+ }
+
+ @Test
+ public void getCorrectValues() {
+ assertEquals(mHbmEvent.getStartTimeMillis(), mStartTimeMillis);
+ assertEquals(mHbmEvent.getEndTimeMillis(), mEndTimeMillis);
+ }
+
+ @Test
+ public void toStringGeneratesExpectedString() {
+ String actualString = mHbmEvent.toString();
+ String expectedString = "HbmEvent: {startTimeMillis:" + mStartTimeMillis
+ + ", endTimeMillis: " + mEndTimeMillis + "}, total: "
+ + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]";
+ assertEquals(actualString, expectedString);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
index 53fa3e2..da2e1be 100644
--- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
@@ -27,9 +27,7 @@
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
import static com.android.server.display.AutomaticBrightnessController
.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE;
-
import static com.android.server.display.DisplayDeviceConfig.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
-
import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID;
import static org.junit.Assert.assertEquals;
@@ -102,6 +100,7 @@
private Binder mDisplayToken;
private String mDisplayUniqueId;
private Context mContextSpy;
+ private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
@Rule
public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
@@ -124,6 +123,7 @@
mTestLooper = new TestLooper(mClock::now);
mDisplayToken = null;
mDisplayUniqueId = "unique_id";
+
mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
when(mContextSpy.getContentResolver()).thenReturn(resolver);
@@ -140,7 +140,8 @@
initHandler(null);
final HighBrightnessModeController hbmc = new HighBrightnessModeController(
mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
- mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy);
+ mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {},
+ null, mContextSpy);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f);
}
@@ -150,7 +151,8 @@
initHandler(null);
final HighBrightnessModeController hbmc = new HighBrightnessModeController(
mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken,
- mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy);
+ mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {},
+ null, mContextSpy);
hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED);
hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
@@ -705,9 +707,12 @@
// Creates instance with standard initialization values.
private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) {
initHandler(clock);
+ if (mHighBrightnessModeMetadata == null) {
+ mHighBrightnessModeMetadata = new HighBrightnessModeMetadata();
+ }
return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH,
DISPLAY_HEIGHT, mDisplayToken, mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX,
- DEFAULT_HBM_DATA, null, () -> {}, mContextSpy);
+ DEFAULT_HBM_DATA, null, () -> {}, mHighBrightnessModeMetadata, mContextSpy);
}
private void initHandler(OffsettableClock clock) {
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
new file mode 100644
index 0000000..ede54e0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class HighBrightnessModeMetadataTest {
+ private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+
+ private long mRunningStartTimeMillis = -1;
+
+ @Before
+ public void setUp() {
+ mHighBrightnessModeMetadata = new HighBrightnessModeMetadata();
+ }
+
+ @Test
+ public void checkDefaultValues() {
+ assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(),
+ mRunningStartTimeMillis);
+ assertEquals(mHighBrightnessModeMetadata.getHbmEventQueue().size(), 0);
+ }
+
+ @Test
+ public void checkSetValues() {
+ mRunningStartTimeMillis = 10;
+ mHighBrightnessModeMetadata.setRunningStartTimeMillis(mRunningStartTimeMillis);
+ assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(),
+ mRunningStartTimeMillis);
+ HbmEvent expectedHbmEvent = new HbmEvent(10, 20);
+ mHighBrightnessModeMetadata.addHbmEvent(expectedHbmEvent);
+ HbmEvent actualHbmEvent = mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst();
+ assertEquals(expectedHbmEvent.toString(), actualHbmEvent.toString());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 638637d..a7da2417 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -16,8 +16,10 @@
package com.android.server.display;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.DEFAULT_DISPLAY_GROUP;
+import static android.view.Display.TYPE_EXTERNAL;
import static android.view.Display.TYPE_INTERNAL;
import static android.view.Display.TYPE_VIRTUAL;
@@ -173,7 +175,7 @@
@Test
public void testDisplayDeviceAddAndRemove_NonInternalTypes() {
- testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_EXTERNAL);
+ testDisplayDeviceAddAndRemove_NonInternal(TYPE_EXTERNAL);
testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_WIFI);
testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_OVERLAY);
testDisplayDeviceAddAndRemove_NonInternal(TYPE_VIRTUAL);
@@ -218,7 +220,7 @@
@Test
public void testDisplayDeviceAddAndRemove_OneExternalDefault() {
- DisplayDevice device = createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800,
+ DisplayDevice device = createDisplayDevice(TYPE_EXTERNAL, 600, 800,
FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
// add
@@ -268,7 +270,7 @@
public void testGetDisplayIdsLocked() {
add(createDisplayDevice(TYPE_INTERNAL, 600, 800,
FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
- add(createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, 0));
+ add(createDisplayDevice(TYPE_EXTERNAL, 600, 800, 0));
add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0));
int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID,
@@ -440,6 +442,11 @@
/* isOverrideActive= */false,
/* isInteractive= */true,
/* isBootCompleted= */true));
+ assertFalse(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED,
+ INVALID_DEVICE_STATE,
+ /* isOverrideActive= */false,
+ /* isInteractive= */true,
+ /* isBootCompleted= */true));
}
@Test
@@ -460,7 +467,7 @@
Layout layout = new Layout();
layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, true, true);
- layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, false);
+ layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, true);
when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
layout = new Layout();
@@ -469,6 +476,8 @@
when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout);
when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout);
+ when(mDeviceStateToLayoutMapSpy.size()).thenReturn(4);
+
LogicalDisplay display1 = add(device1);
assertEquals(info(display1).address, info(device1).address);
assertEquals(DEFAULT_DISPLAY, id(display1));
@@ -481,8 +490,15 @@
mLogicalDisplayMapper.setDeviceStateLocked(0, false);
mLooper.moveTimeForward(1000);
mLooper.dispatchAll();
+ // The new state is not applied until the boot is completed
assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
+
+ mLogicalDisplayMapper.onBootCompleted();
+ mLooper.moveTimeForward(1000);
+ mLooper.dispatchAll();
+ assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
+ assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
@@ -581,6 +597,7 @@
// 2) Mark the displays as STATE_OFF so that it can continue with transition
// 3) Send DISPLAY_DEVICE_EVENT_CHANGE to inform the mapper of the new display state
// 4) Dispatch handler events.
+ mLogicalDisplayMapper.onBootCompleted();
mLogicalDisplayMapper.setDeviceStateLocked(0, false);
mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED);
mLooper.moveTimeForward(1000);
@@ -623,6 +640,23 @@
assertEquals(3, threeDisplaysEnabled.length);
}
+ @Test
+ public void testCreateNewLogicalDisplay() {
+ DisplayDevice device1 = createDisplayDevice(TYPE_EXTERNAL, 600, 800,
+ FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+ when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1);
+ LogicalDisplay display1 = add(device1);
+
+ assertTrue(display1.isEnabledLocked());
+
+ DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+ FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+ when(mDeviceStateToLayoutMapSpy.size()).thenReturn(2);
+ LogicalDisplay display2 = add(device2);
+
+ assertFalse(display2.isEnabledLocked());
+ }
+
/////////////////
// Helper Methods
/////////////////
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
new file mode 100644
index 0000000..6c73f71
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamOverlayServiceTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.server.dreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.dreams.DreamOverlayService;
+import android.service.dreams.IDreamOverlay;
+import android.service.dreams.IDreamOverlayCallback;
+import android.service.dreams.IDreamOverlayClient;
+import android.service.dreams.IDreamOverlayClientCallback;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * A collection of tests to exercise {@link DreamOverlayService}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamOverlayServiceTest {
+ private static final ComponentName FIRST_DREAM_COMPONENT =
+ ComponentName.unflattenFromString("com.foo.bar/.DreamService");
+ private static final ComponentName SECOND_DREAM_COMPONENT =
+ ComponentName.unflattenFromString("com.foo.baz/.DreamService");
+
+ @Mock
+ WindowManager.LayoutParams mLayoutParams;
+
+ @Mock
+ IDreamOverlayCallback mOverlayCallback;
+
+ /**
+ * {@link TestDreamOverlayService} is a simple {@link DreamOverlayService} implementation for
+ * tracking interactions across {@link IDreamOverlay} binder interface. The service reports
+ * interactions to a {@link Monitor} instance provided at construction.
+ */
+ private static class TestDreamOverlayService extends DreamOverlayService {
+ /**
+ * An interface implemented to be informed when the corresponding methods in
+ * {@link TestDreamOverlayService} are invoked.
+ */
+ interface Monitor {
+ void onStartDream();
+ void onEndDream();
+ void onWakeUp();
+ }
+
+ private final Monitor mMonitor;
+
+ TestDreamOverlayService(Monitor monitor) {
+ super();
+ mMonitor = monitor;
+ }
+
+ @Override
+ public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
+ mMonitor.onStartDream();
+ }
+
+ @Override
+ public void onEndDream() {
+ mMonitor.onEndDream();
+ super.onEndDream();
+ }
+
+ @Override
+ public void onWakeUp(@NonNull Runnable onCompleteCallback) {
+ mMonitor.onWakeUp();
+ super.onWakeUp(onCompleteCallback);
+ }
+ }
+
+ /**
+ * A {@link IDreamOverlayClientCallback} implementation that captures the requested client.
+ */
+ private static class OverlayClientCallback extends IDreamOverlayClientCallback.Stub {
+ public IDreamOverlayClient retrievedClient;
+ @Override
+ public void onDreamOverlayClient(IDreamOverlayClient client) throws RemoteException {
+ retrievedClient = client;
+ }
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ /**
+ * Verifies that only the currently started dream is able to affect the overlay.
+ */
+ @Test
+ public void testOverlayClientInteraction() throws RemoteException {
+ final TestDreamOverlayService.Monitor monitor = Mockito.mock(
+ TestDreamOverlayService.Monitor.class);
+ final TestDreamOverlayService service = new TestDreamOverlayService(monitor);
+ final IBinder binder = service.onBind(new Intent());
+ final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder);
+
+ // Create two overlay clients and ensure they are unique.
+ final IDreamOverlayClient firstClient = getClient(overlay);
+ assertThat(firstClient).isNotNull();
+
+ final IDreamOverlayClient secondClient = getClient(overlay);
+ assertThat(secondClient).isNotNull();
+
+ assertThat(firstClient).isNotEqualTo(secondClient);
+
+ // Start a dream with the first client and ensure the dream is now active from the
+ // overlay's perspective.
+ firstClient.startDream(mLayoutParams, mOverlayCallback,
+ FIRST_DREAM_COMPONENT.flattenToString(), false);
+
+
+ verify(monitor).onStartDream();
+ assertThat(service.getDreamComponent()).isEqualTo(FIRST_DREAM_COMPONENT);
+
+ Mockito.clearInvocations(monitor);
+
+ // Start a dream from the second client and verify that the overlay has both cycled to
+ // the new dream (ended/started).
+ secondClient.startDream(mLayoutParams, mOverlayCallback,
+ SECOND_DREAM_COMPONENT.flattenToString(), false);
+
+ verify(monitor).onEndDream();
+ verify(monitor).onStartDream();
+ assertThat(service.getDreamComponent()).isEqualTo(SECOND_DREAM_COMPONENT);
+
+ Mockito.clearInvocations(monitor);
+
+ // Verify that interactions with the first, now inactive client don't affect the overlay.
+ firstClient.endDream();
+ verify(monitor, never()).onEndDream();
+
+ firstClient.wakeUp();
+ verify(monitor, never()).onWakeUp();
+ }
+
+ private static IDreamOverlayClient getClient(IDreamOverlay overlay) throws RemoteException {
+ final OverlayClientCallback callback = new OverlayClientCallback();
+ overlay.getClient(callback);
+ return callback.retrievedClient;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
index 96302b9..299f153 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
@@ -25,6 +25,7 @@
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
@@ -63,6 +64,7 @@
private MatrixCursor mContactsLookupCursor;
private MatrixCursor mPhoneCursor;
private ContactsQueryHelper mHelper;
+ private ContactsContentProvider contentProvider;
@Before
public void setUp() {
@@ -73,7 +75,7 @@
mPhoneCursor = new MatrixCursor(PHONE_COLUMNS);
MockContentResolver contentResolver = new MockContentResolver();
- ContactsContentProvider contentProvider = new ContactsContentProvider();
+ contentProvider = new ContactsContentProvider();
contentProvider.registerCursor(Contacts.CONTENT_URI, mContactsCursor);
contentProvider.registerCursor(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI, mContactsLookupCursor);
@@ -89,6 +91,14 @@
}
@Test
+ public void testQueryException_returnsFalse() {
+ contentProvider.setThrowException(true);
+
+ Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY);
+ assertFalse(mHelper.query(contactUri.toString()));
+ }
+
+ @Test
public void testQueryWithUri() {
mContactsCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 1,
@@ -168,10 +178,15 @@
private class ContactsContentProvider extends MockContentProvider {
private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>();
+ private boolean throwException = false;
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ if (throwException) {
+ throw new SQLiteException();
+ }
+
for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) {
if (uri.isPathPrefixMatch(prefixUri)) {
return mUriPrefixToCursorMap.get(prefixUri);
@@ -180,6 +195,10 @@
return mUriPrefixToCursorMap.get(uri);
}
+ public void setThrowException(boolean throwException) {
+ this.throwException = throwException;
+ }
+
private void registerCursor(Uri uriPrefix, Cursor cursor) {
mUriPrefixToCursorMap.put(uriPrefix, cursor);
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 66c3f07..a27602d 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -500,6 +500,7 @@
// The cached conversations are above the limit because every conversation has active
// notifications. To uncache one of them, the notifications for that conversation need to
// be dismissed.
+ String notificationKey = "";
for (int i = 0; i < DataManager.MAX_CACHED_RECENT_SHORTCUTS + 1; i++) {
String shortcutId = TEST_SHORTCUT_ID + i;
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, shortcutId,
@@ -507,11 +508,13 @@
shortcut.setCached(ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
mDataManager.addOrUpdateConversationInfo(shortcut);
when(mNotification.getShortcutId()).thenReturn(shortcutId);
- sendGenericNotification();
+ notificationKey = String.format("notification-key-%d", i);
+ sendGenericNotificationWithKey(notificationKey);
}
// Post another notification for the last conversation.
- sendGenericNotification();
+ String otherNotificationKey = "other-notification-key";
+ sendGenericNotificationWithKey(otherNotificationKey);
// Removing one of the two notifications does not un-cache the shortcut.
listenerService.onNotificationRemoved(mGenericSbn, null,
@@ -520,6 +523,7 @@
anyInt(), any(), anyString(), any(), anyInt(), anyInt());
// Removing the second notification un-caches the shortcut.
+ when(mGenericSbn.getKey()).thenReturn(notificationKey);
listenerService.onNotificationRemoved(mGenericSbn, null,
NotificationListenerService.REASON_CANCEL_ALL);
verify(mShortcutServiceInternal).uncacheShortcuts(
@@ -687,6 +691,63 @@
}
@Test
+ public void testGetConversation_trackActiveConversations() {
+ mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+ assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+ TEST_SHORTCUT_ID)).isNull();
+ ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+ buildPerson());
+ shortcut.setCached(ShortcutInfo.FLAG_PINNED);
+ mDataManager.addOrUpdateConversationInfo(shortcut);
+ assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+ TEST_SHORTCUT_ID)).isNotNull();
+
+ sendGenericNotification();
+ sendGenericNotification();
+ ConversationChannel result = mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+ TEST_SHORTCUT_ID);
+ assertTrue(result.hasActiveNotifications());
+
+ // Both generic notifications have the same notification key, so a single dismiss will
+ // remove both of them.
+ NotificationListenerService listenerService =
+ mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
+ listenerService.onNotificationRemoved(mGenericSbn, null,
+ NotificationListenerService.REASON_CANCEL);
+ ConversationChannel resultTwo = mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+ TEST_SHORTCUT_ID);
+ assertFalse(resultTwo.hasActiveNotifications());
+ }
+
+ @Test
+ public void testGetConversation_unsyncedShortcut() {
+ mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+ ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+ buildPerson());
+ shortcut.setCached(ShortcutInfo.FLAG_PINNED);
+ mDataManager.addOrUpdateConversationInfo(shortcut);
+ assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+ TEST_SHORTCUT_ID)).isNotNull();
+ assertThat(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
+ .getConversationStore()
+ .getConversation(TEST_SHORTCUT_ID)).isNotNull();
+
+ when(mShortcutServiceInternal.getShortcuts(
+ anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
+ anyInt(), anyInt(), anyInt(), anyInt()))
+ .thenReturn(Collections.emptyList());
+ assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
+ TEST_SHORTCUT_ID)).isNull();
+
+ // Conversation is removed from store as there is no matching shortcut in ShortcutManager
+ assertThat(mDataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
+ .getConversationStore()
+ .getConversation(TEST_SHORTCUT_ID)).isNull();
+ verify(mNotificationManagerInternal)
+ .onConversationRemoved(TEST_PKG_NAME, TEST_PKG_UID, Set.of(TEST_SHORTCUT_ID));
+ }
+
+ @Test
public void testOnNotificationChannelModified() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
assertThat(mDataManager.getConversation(TEST_PKG_NAME, USER_ID_PRIMARY,
@@ -1294,7 +1355,7 @@
sendGenericNotification();
- mDataManager.getRecentConversations(USER_ID_PRIMARY);
+ mDataManager.getRecentConversations(USER_ID_PRIMARY);
verify(mShortcutServiceInternal).getShortcuts(
anyInt(), anyString(), anyLong(), anyString(), anyList(), any(), any(),
@@ -1665,6 +1726,12 @@
// "Sends" a notification to a non-customized notification channel - the notification channel
// is something generic like "messages" and the notification has a shortcut id
private void sendGenericNotification() {
+ sendGenericNotificationWithKey(GENERIC_KEY);
+ }
+
+ // "Sends" a notification to a non-customized notification channel with the specified key.
+ private void sendGenericNotificationWithKey(String key) {
+ when(mGenericSbn.getKey()).thenReturn(key);
when(mNotification.getChannelId()).thenReturn(PARENT_NOTIFICATION_CHANNEL_ID);
doAnswer(invocationOnMock -> {
NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking)
@@ -1678,9 +1745,9 @@
mParentNotificationChannel.getImportance(),
null, null,
mParentNotificationChannel, null, null, true, 0, false, -1, false, null, null,
- false, false, false, null, 0, false);
+ false, false, false, null, 0, false, 0);
return true;
- }).when(mRankingMap).getRanking(eq(GENERIC_KEY),
+ }).when(mRankingMap).getRanking(eq(key),
any(NotificationListenerService.Ranking.class));
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
@@ -1704,7 +1771,7 @@
mNotificationChannel.getImportance(),
null, null,
mNotificationChannel, null, null, true, 0, false, -1, false, null, null, false,
- false, false, null, 0, false);
+ false, false, null, 0, false, 0);
return true;
}).when(mRankingMap).getRanking(eq(CUSTOM_KEY),
any(NotificationListenerService.Ranking.class));
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java
index 3de65c1..1b3a199 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowOrientationListenerTest.java
@@ -132,6 +132,19 @@
assertThat(mFinalizedRotation).isEqualTo(DEFAULT_SENSOR_ROTATION);
}
+ @Test
+ public void testOnSensorChanged_screenLocked_doNotCallRotationResolverReturnDefaultRotation() {
+ mWindowOrientationListener = new TestableWindowOrientationListener(mMockContext,
+ mHandler, /* defaultRotation */ Surface.ROTATION_180);
+ mWindowOrientationListener.mRotationResolverService = mFakeRotationResolverInternal;
+ mWindowOrientationListener.mIsScreenLocked = true;
+
+ mWindowOrientationListener.mOrientationJudge.onSensorChanged(mFakeSensorEvent);
+
+ assertThat(mWindowOrientationListener.mIsOnProposedRotationChangedCalled).isFalse();
+ assertThat(mFinalizedRotation).isEqualTo(Surface.ROTATION_180);
+ }
+
static final class TestableRotationResolver extends RotationResolverInternal {
@Surface.Rotation
RotationResolverCallbackInternal mCallback;
@@ -166,21 +179,17 @@
}
}
- @Test
- public void testOnSensorChanged_inLockScreen_doNotCallRotationResolver() {
- mWindowOrientationListener.mIsScreenLocked = true;
-
- mWindowOrientationListener.mOrientationJudge.onSensorChanged(mFakeSensorEvent);
-
- assertThat(mWindowOrientationListener.mIsOnProposedRotationChangedCalled).isFalse();
- }
-
final class TestableWindowOrientationListener extends WindowOrientationListener {
private boolean mIsOnProposedRotationChangedCalled = false;
private boolean mIsScreenLocked;
TestableWindowOrientationListener(Context context, Handler handler) {
- super(context, handler);
+ this(context, handler, Surface.ROTATION_0);
+ }
+
+ TestableWindowOrientationListener(Context context, Handler handler,
+ @Surface.Rotation int defaultRotation) {
+ super(context, handler, defaultRotation);
this.mOrientationJudge = new OrientationSensorJudge();
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 7986043..8b1384e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -1582,6 +1582,55 @@
}
@Test
+ public void testSetComponentState_differentUsers() throws Exception {
+ Context context = mock(Context.class);
+ PackageManager pm = mock(PackageManager.class);
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
+ when(context.getPackageName()).thenReturn(mContext.getPackageName());
+ when(context.getUserId()).thenReturn(mContext.getUserId());
+ when(context.getPackageManager()).thenReturn(pm);
+ when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
+
+ ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+ ComponentName cn = ComponentName.unflattenFromString("a/a");
+
+ addExpectedServices(service, Arrays.asList("a"), mZero.id);
+ addExpectedServices(service, Arrays.asList("a"), mTen.id);
+ when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ ServiceConnection sc = (ServiceConnection) args[1];
+ sc.onServiceConnected(cn, mock(IBinder.class));
+ return true;
+ });
+ service.addApprovedList("a/a", 0, true);
+ service.addApprovedList("a/a", 10, false);
+
+ service.registerService(cn, mZero.id);
+ assertTrue(service.isBound(cn, mZero.id));
+
+ service.onUserSwitched(mTen.id);
+ assertFalse(service.isBound(cn, mZero.id));
+ service.registerService(cn, mTen.id);
+ assertTrue(service.isBound(cn, mTen.id));
+
+ service.setComponentState(cn, mTen.id, false);
+ assertFalse(service.isBound(cn, mZero.id));
+ assertFalse(service.isBound(cn, mTen.id));
+
+ // Service should be rebound on user 0, since it was only disabled for user 10.
+ service.onUserSwitched(mZero.id);
+ assertTrue(service.isBound(cn, mZero.id));
+ assertFalse(service.isBound(cn, mTen.id));
+
+ // Service should stay unbound on going back to user 10.
+ service.onUserSwitched(mTen.id);
+ assertFalse(service.isBound(cn, mZero.id));
+ assertFalse(service.isBound(cn, mTen.id));
+ }
+ @Test
public void testOnPackagesChanged_nullValuesPassed_noNullPointers() {
for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1847,7 +1896,7 @@
}
private void addExpectedServices(final ManagedServices service, final List<String> packages,
- int userId) {
+ int userId) throws Exception {
ManagedServices.Config config = service.getConfig();
when(mPm.queryIntentServicesAsUser(any(), anyInt(), eq(userId))).
thenAnswer(new Answer<List<ResolveInfo>>() {
@@ -1876,6 +1925,20 @@
return new ArrayList<>();
}
});
+
+ when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
+ (Answer<ServiceInfo>) invocation -> {
+ ComponentName invocationCn = invocation.getArgument(0);
+ if (invocationCn != null && packages.contains(invocationCn.getPackageName())) {
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = invocationCn.getPackageName();
+ serviceInfo.name = invocationCn.getClassName();
+ serviceInfo.permission = service.getConfig().bindPermission;
+ return serviceInfo;
+ }
+ return null;
+ }
+ );
}
private List<String> stringToList(String list) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index 12cd834..8a99c2c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -193,7 +193,8 @@
tweak.isConversation(),
tweak.getConversationShortcutInfo(),
tweak.getRankingAdjustment(),
- tweak.isBubble()
+ tweak.isBubble(),
+ tweak.getProposedImportance()
);
assertNotEquals(nru, nru2);
}
@@ -274,7 +275,8 @@
isConversation(i),
getShortcutInfo(i),
getRankingAdjustment(i),
- isBubble(i)
+ isBubble(i),
+ getProposedImportance(i)
);
rankings[i] = ranking;
}
@@ -402,6 +404,10 @@
return index % 3 - 1;
}
+ private int getProposedImportance(int index) {
+ return index % 5 - 1;
+ }
+
private boolean isBubble(int index) {
return index % 4 == 0;
}
@@ -443,6 +449,7 @@
assertEquals(comment, a.getConversationShortcutInfo().getId(),
b.getConversationShortcutInfo().getId());
assertActionsEqual(a.getSmartActions(), b.getSmartActions());
+ assertEquals(a.getProposedImportance(), b.getProposedImportance());
}
private void detailedAssertEquals(RankingMap a, RankingMap b) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
new file mode 100644
index 0000000..87e86cb
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.server.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.notification.Adjustment;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
+
+ @Test
+ public void testHasDiffs_noDiffs() {
+ NotificationRecord r = generateRecord();
+
+ NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+ 1,
+ r.getPackageVisibilityOverride(),
+ r.canShowBadge(),
+ r.canBubble(),
+ r.getNotification().isBubbleNotification(),
+ r.getChannel(),
+ r.getGroupKey(),
+ r.getPeopleOverride(),
+ r.getSnoozeCriteria(),
+ r.getUserSentiment(),
+ r.getSuppressedVisualEffects(),
+ r.getSystemGeneratedSmartActions(),
+ r.getSmartReplies(),
+ r.getImportance(),
+ r.getRankingScore(),
+ r.isConversation(),
+ r.getProposedImportance());
+
+ assertFalse(extractorData.hasDiffForRankingLocked(r, 1));
+ assertFalse(extractorData.hasDiffForLoggingLocked(r, 1));
+ }
+
+ @Test
+ public void testHasDiffs_proposedImportanceChange() {
+ NotificationRecord r = generateRecord();
+
+ NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+ 1,
+ r.getPackageVisibilityOverride(),
+ r.canShowBadge(),
+ r.canBubble(),
+ r.getNotification().isBubbleNotification(),
+ r.getChannel(),
+ r.getGroupKey(),
+ r.getPeopleOverride(),
+ r.getSnoozeCriteria(),
+ r.getUserSentiment(),
+ r.getSuppressedVisualEffects(),
+ r.getSystemGeneratedSmartActions(),
+ r.getSmartReplies(),
+ r.getImportance(),
+ r.getRankingScore(),
+ r.isConversation(),
+ r.getProposedImportance());
+
+ Bundle signals = new Bundle();
+ signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH);
+ Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
+ r.addAdjustment(adjustment);
+ r.applyAdjustments();
+
+ assertTrue(extractorData.hasDiffForRankingLocked(r, 1));
+ assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
+ }
+
+ private NotificationRecord generateRecord() {
+ NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
+ final Notification.Builder builder = new Notification.Builder(getContext())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ Notification n = builder.build();
+ StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0,
+ 0, n, UserHandle.ALL, null, System.currentTimeMillis());
+ return new NotificationRecord(getContext(), sbn, channel);
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index 5468220..14b0048 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -19,6 +19,7 @@
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.service.notification.Adjustment.KEY_IMPORTANCE;
import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -755,6 +756,24 @@
}
@Test
+ public void testProposedImportance() {
+ StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
+ true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+ false /* lights */, false /* defaultLights */, groupId /* group */);
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+
+ assertEquals(IMPORTANCE_UNSPECIFIED, record.getProposedImportance());
+
+ Bundle signals = new Bundle();
+ signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_DEFAULT);
+ record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId()));
+
+ record.applyAdjustments();
+
+ assertEquals(IMPORTANCE_DEFAULT, record.getProposedImportance());
+ }
+
+ @Test
public void testAppImportance_returnsCorrectly() {
StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index d46530c..d824fee 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -1703,6 +1703,7 @@
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
channel.setShowBadge(true);
channel.setAllowBubbles(false);
+ channel.setImportantConversation(true);
int lockMask = 0;
for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
lockMask |= NotificationChannel.LOCKABLE_FIELDS[i];
@@ -1718,6 +1719,7 @@
assertEquals(channel.shouldShowLights(), savedChannel.shouldShowLights());
assertFalse(savedChannel.canBypassDnd());
assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility());
+ assertFalse(channel.isImportantConversation());
assertEquals(channel.canShowBadge(), savedChannel.canShowBadge());
assertEquals(channel.canBubble(), savedChannel.canBubble());
@@ -4396,7 +4398,7 @@
new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
channel2.setConversationId(calls.getId(), convoId);
channel2.setImportantConversation(true);
- mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false);
List<ConversationChannelWrapper> convos =
mHelper.getConversations(IntArray.wrap(new int[] {0}), false);
@@ -4473,7 +4475,7 @@
new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
channel2.setConversationId(calls.getId(), convoId);
channel2.setImportantConversation(true);
- mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false);
List<ConversationChannelWrapper> convos =
mHelper.getConversations(IntArray.wrap(new int[] {0}), false);
@@ -4501,13 +4503,13 @@
new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT);
channel.setConversationId(messages.getId(), convoId);
channel.setImportantConversation(true);
- mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false);
NotificationChannel diffConvo =
new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT);
diffConvo.setConversationId(p.getId(), "different convo");
diffConvo.setImportantConversation(true);
- mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false);
+ mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, false, false);
NotificationChannel channel2 =
new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT);
@@ -4534,7 +4536,7 @@
new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT);
channel.setConversationId(messages.getId(), convoId);
channel.setImportantConversation(true);
- mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false);
mHelper.permanentlyDeleteNotificationChannel(PKG_O, UID_O, "messages");
@@ -4935,7 +4937,7 @@
"conversation", IMPORTANCE_DEFAULT);
friend.setConversationId(parent.getId(), "friend");
friend.setImportantConversation(true);
- mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false);
+ mHelper.createNotificationChannel(PKG_O, UID_O, friend, false, false);
ArrayList<StatsEvent> events = new ArrayList<>();
mHelper.pullPackageChannelPreferencesStats(events);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index eed32d7..bb20244 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -249,6 +249,19 @@
}
/**
+ * Ensures it updates recent tasks order when the last resumed activity changed.
+ */
+ @Test
+ public void testUpdateRecentTasksForTopResumed() {
+ spyOn(mSupervisor.mRecentTasks);
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ final Task task = activity.getTask();
+
+ mAtm.setLastResumedActivityUncheckLocked(activity, "test");
+ verify(mSupervisor.mRecentTasks).add(eq(task));
+ }
+
+ /**
* Ensures that a trusted display can launch arbitrary activity and an untrusted display can't.
*/
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index e30e5db..74ea7d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -40,7 +40,7 @@
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.view.WindowManager;
-import android.window.BackEvent;
+import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
import android.window.IOnBackInvokedCallback;
import android.window.OnBackInvokedCallback;
@@ -242,11 +242,11 @@
private IOnBackInvokedCallback createOnBackInvokedCallback() {
return new IOnBackInvokedCallback.Stub() {
@Override
- public void onBackStarted(BackEvent backEvent) {
+ public void onBackStarted(BackMotionEvent backEvent) {
}
@Override
- public void onBackProgressed(BackEvent backEvent) {
+ public void onBackProgressed(BackMotionEvent backEvent) {
}
@Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
index 86732c9..2a28ae2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java
@@ -16,13 +16,12 @@
package com.android.server.wm;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import android.content.Context;
import android.content.res.Resources;
@@ -32,9 +31,10 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.R;
+
import org.junit.Before;
import org.junit.Test;
-import org.mockito.ArgumentCaptor;
import java.util.function.Consumer;
@@ -48,92 +48,76 @@
@Presubmit
public class DeviceStateControllerTests {
- private DeviceStateController.FoldStateListener mFoldStateListener;
private DeviceStateController mTarget;
private DeviceStateControllerBuilder mBuilder;
private Context mMockContext;
- private Handler mMockHandler;
- private Resources mMockRes;
private DeviceStateManager mMockDeviceStateManager;
-
- private Consumer<DeviceStateController.FoldState> mDelegate;
- private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+ private DeviceStateController.DeviceState mCurrentState =
+ DeviceStateController.DeviceState.UNKNOWN;
@Before
public void setUp() {
mBuilder = new DeviceStateControllerBuilder();
- mCurrentState = DeviceStateController.FoldState.UNKNOWN;
+ mCurrentState = DeviceStateController.DeviceState.UNKNOWN;
}
- private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception {
+ private void initialize(boolean supportFold, boolean supportHalfFold) {
mBuilder.setSupportFold(supportFold, supportHalfFold);
- mDelegate = (newFoldState) -> {
+ Consumer<DeviceStateController.DeviceState> delegate = (newFoldState) -> {
mCurrentState = newFoldState;
};
- mBuilder.setDelegate(mDelegate);
+ mBuilder.setDelegate(delegate);
mBuilder.build();
- verifyFoldStateListenerRegistration(1);
+ verify(mMockDeviceStateManager).registerCallback(any(), any());
}
@Test
- public void testInitialization() throws Exception {
+ public void testInitialization() {
initialize(true /* supportFold */, true /* supportHalfFolded */);
- mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ mTarget.onStateChanged(mOpenDeviceStates[0]);
+ assertEquals(DeviceStateController.DeviceState.OPEN, mCurrentState);
}
@Test
- public void testInitializationWithNoFoldSupport() throws Exception {
+ public void testInitializationWithNoFoldSupport() {
initialize(false /* supportFold */, false /* supportHalfFolded */);
- mFoldStateListener.onStateChanged(mFoldedStates[0]);
+ mTarget.onStateChanged(mFoldedStates[0]);
// Note that the folded state is ignored.
- assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
+ assertEquals(DeviceStateController.DeviceState.UNKNOWN, mCurrentState);
}
@Test
- public void testWithFoldSupported() throws Exception {
+ public void testWithFoldSupported() {
initialize(true /* supportFold */, false /* supportHalfFolded */);
- mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
- mFoldStateListener.onStateChanged(mFoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
- mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored
+ mTarget.onStateChanged(mOpenDeviceStates[0]);
+ assertEquals(DeviceStateController.DeviceState.OPEN, mCurrentState);
+ mTarget.onStateChanged(mFoldedStates[0]);
+ assertEquals(DeviceStateController.DeviceState.FOLDED, mCurrentState);
+ mTarget.onStateChanged(mHalfFoldedStates[0]);
+ assertEquals(DeviceStateController.DeviceState.UNKNOWN, mCurrentState); // Ignored
}
@Test
- public void testWithHalfFoldSupported() throws Exception {
+ public void testWithHalfFoldSupported() {
initialize(true /* supportFold */, true /* supportHalfFolded */);
- mFoldStateListener.onStateChanged(mUnfoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN);
- mFoldStateListener.onStateChanged(mFoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED);
- mFoldStateListener.onStateChanged(mHalfFoldedStates[0]);
- assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED);
+ mTarget.onStateChanged(mOpenDeviceStates[0]);
+ assertEquals(DeviceStateController.DeviceState.OPEN, mCurrentState);
+ mTarget.onStateChanged(mFoldedStates[0]);
+ assertEquals(DeviceStateController.DeviceState.FOLDED, mCurrentState);
+ mTarget.onStateChanged(mHalfFoldedStates[0]);
+ assertEquals(DeviceStateController.DeviceState.HALF_FOLDED, mCurrentState);
}
-
private final int[] mFoldedStates = {0};
- private final int[] mUnfoldedStates = {1};
+ private final int[] mOpenDeviceStates = {1};
private final int[] mHalfFoldedStates = {2};
-
-
- private void verifyFoldStateListenerRegistration(int numOfInvocation) {
- final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor =
- ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class);
- verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback(
- any(),
- listenerCaptor.capture());
- if (numOfInvocation > 0) {
- mFoldStateListener = listenerCaptor.getValue();
- }
- }
+ private final int[] mRearDisplayStates = {3};
private class DeviceStateControllerBuilder {
private boolean mSupportFold = false;
private boolean mSupportHalfFold = false;
- private Consumer<DeviceStateController.FoldState> mDelegate;
+ private Consumer<DeviceStateController.DeviceState> mDelegate;
DeviceStateControllerBuilder setSupportFold(
boolean supportFold, boolean supportHalfFold) {
@@ -143,34 +127,44 @@
}
DeviceStateControllerBuilder setDelegate(
- Consumer<DeviceStateController.FoldState> delegate) {
+ Consumer<DeviceStateController.DeviceState> delegate) {
mDelegate = delegate;
return this;
}
private void mockFold(boolean enableFold, boolean enableHalfFold) {
+ if (enableFold || enableHalfFold) {
+ when(mMockContext.getResources()
+ .getIntArray(R.array.config_openDeviceStates))
+ .thenReturn(mOpenDeviceStates);
+ when(mMockContext.getResources()
+ .getIntArray(R.array.config_rearDisplayDeviceStates))
+ .thenReturn(mRearDisplayStates);
+ }
+
if (enableFold) {
- when(mMockContext.getResources().getIntArray(
- com.android.internal.R.array.config_foldedDeviceStates))
+ when(mMockContext.getResources()
+ .getIntArray(R.array.config_foldedDeviceStates))
.thenReturn(mFoldedStates);
}
if (enableHalfFold) {
- when(mMockContext.getResources().getIntArray(
- com.android.internal.R.array.config_halfFoldedDeviceStates))
+ when(mMockContext.getResources()
+ .getIntArray(R.array.config_halfFoldedDeviceStates))
.thenReturn(mHalfFoldedStates);
}
}
- private void build() throws Exception {
+ private void build() {
mMockContext = mock(Context.class);
- mMockRes = mock(Resources.class);
- when(mMockContext.getResources()).thenReturn((mMockRes));
mMockDeviceStateManager = mock(DeviceStateManager.class);
when(mMockContext.getSystemService(DeviceStateManager.class))
.thenReturn(mMockDeviceStateManager);
+ Resources mockRes = mock(Resources.class);
+ when(mMockContext.getResources()).thenReturn((mockRes));
mockFold(mSupportFold, mSupportHalfFold);
- mMockHandler = mock(Handler.class);
- mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate);
+ Handler mockHandler = mock(Handler.class);
+ mTarget = new DeviceStateController(mMockContext, mockHandler);
+ mTarget.registerDeviceStateCallback(mDelegate);
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 98e68ca..dc12469 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1736,7 +1736,7 @@
// No need to apply rotation if the display ignores orientation request.
doCallRealMethod().when(displayContent).rotationForActivityInDifferentOrientation(any());
- pinnedActivity.mOrientation = SCREEN_ORIENTATION_LANDSCAPE;
+ pinnedActivity.setOverrideOrientation(SCREEN_ORIENTATION_LANDSCAPE);
displayContent.setIgnoreOrientationRequest(true);
assertEquals(WindowConfiguration.ROTATION_UNDEFINED,
displayContent.rotationForActivityInDifferentOrientation(pinnedActivity));
@@ -2064,7 +2064,8 @@
// Update the forced size and density in settings and the unique id to simualate a display
// remap.
dc.mWmService.mDisplayWindowSettings.setForcedSize(dc, forcedWidth, forcedHeight);
- dc.mWmService.mDisplayWindowSettings.setForcedDensity(dc, forcedDensity, 0 /* userId */);
+ dc.mWmService.mDisplayWindowSettings.setForcedDensity(displayInfo, forcedDensity,
+ 0 /* userId */);
dc.mCurrentUniqueDisplayId = mDisplayInfo.uniqueId + "-test";
// Trigger display changed.
dc.onDisplayChanged();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 8bb79e3..45b30b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -155,6 +155,18 @@
}
@Test
+ public void testTreatmentDisabledPerApp_noForceRotationOrRefresh()
+ throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ when(mActivity.mLetterboxUiController.shouldForceRotateForCameraCompat())
+ .thenReturn(false);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent);
@@ -327,7 +339,21 @@
}
@Test
- public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+ public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
+ throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ when(mActivity.mLetterboxUiController.shouldRefreshActivityForCameraCompat())
+ .thenReturn(false);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh()
+ throws Exception {
when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -362,6 +388,19 @@
assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
}
+ @Test
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
+ throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat())
+ .thenReturn(true);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ }
+
private void configureActivity(@ScreenOrientation int activityOrientation) {
configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index 4ce43e1..ed2b0a3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -56,6 +56,7 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceStateManager;
import android.os.PowerManagerInternal;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
@@ -111,6 +112,7 @@
private ContentResolver mMockResolver;
private FakeSettingsProvider mFakeSettingsProvider;
private StatusBarManagerInternal mMockStatusBarManagerInternal;
+ private DeviceStateManager mMockDeviceStateManager;
// Fields below are callbacks captured from test target.
private ContentObserver mShowRotationSuggestionsObserver;
@@ -120,6 +122,7 @@
private DisplayRotationBuilder mBuilder;
+ private DeviceStateController mDeviceStateController;
private DisplayRotation mTarget;
@BeforeClass
@@ -484,6 +487,34 @@
SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
}
+ @Test
+ public void testReverseRotation() throws Exception {
+ mBuilder.build();
+ configureDisplayRotation(SCREEN_ORIENTATION_PORTRAIT, false, false);
+
+ when(mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()).thenReturn(true);
+
+ thawRotation();
+
+ enableOrientationSensor();
+
+ mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_90));
+ assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_270));
+ assertEquals(Surface.ROTATION_90, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
+ assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
+
+ mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_180));
+ assertEquals(Surface.ROTATION_180, mTarget.rotationForOrientation(
+ SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_180));
+ }
+
private boolean waitForUiHandler() {
final CountDownLatch latch = new CountDownLatch(1);
UiThread.getHandler().post(latch::countDown);
@@ -705,7 +736,7 @@
enableOrientationSensor();
- mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+ mTarget.foldStateChanged(DeviceStateController.DeviceState.OPEN);
freezeRotation(Surface.ROTATION_270);
mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0));
@@ -715,7 +746,7 @@
SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
// ... until half-fold
- mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED);
+ mTarget.foldStateChanged(DeviceStateController.DeviceState.HALF_FOLDED);
assertTrue(waitForUiHandler());
verify(sMockWm).updateRotation(false, false);
assertTrue(waitForUiHandler());
@@ -723,7 +754,7 @@
SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0));
// ... then transition back to flat
- mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN);
+ mTarget.foldStateChanged(DeviceStateController.DeviceState.OPEN);
assertTrue(waitForUiHandler());
verify(sMockWm, atLeast(1)).updateRotation(false, false);
assertTrue(waitForUiHandler());
@@ -1097,8 +1128,14 @@
mMockDisplayWindowSettings = mock(DisplayWindowSettings.class);
+ mMockDeviceStateManager = mock(DeviceStateManager.class);
+ when(mMockContext.getSystemService(eq(DeviceStateManager.class)))
+ .thenReturn(mMockDeviceStateManager);
+
+ mDeviceStateController = mock(DeviceStateController.class);
mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress,
- mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object()) {
+ mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object(),
+ mDeviceStateController) {
@Override
DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy(
WindowManagerService service, DisplayContent displayContent) {
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 c398a0a..fb4f2ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -236,8 +236,8 @@
@Test
public void testSetForcedDensity() {
- mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay, 600 /* density */,
- 0 /* userId */);
+ mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(),
+ 600 /* density */, 0 /* userId */);
mDisplayWindowSettings.applySettingsToDisplayLocked(mSecondaryDisplay);
assertEquals(600 /* density */, mSecondaryDisplay.mBaseDisplayDensity);
@@ -439,8 +439,9 @@
public void testDisplayWindowSettingsAppliedOnDisplayReady() {
// Set forced densities for two displays in DisplayWindowSettings
final DisplayContent dc = createMockSimulatedDisplay();
- mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay, 123, 0 /* userId */);
- mDisplayWindowSettings.setForcedDensity(dc, 456, 0 /* userId */);
+ mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay.getDisplayInfo(), 123,
+ 0 /* userId */);
+ mDisplayWindowSettings.setForcedDensity(dc.getDisplayInfo(), 456, 0 /* userId */);
// Apply settings to displays - the settings will be stored, but config will not be
// recalculated immediately.
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 6d778afe..c7f19fb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -16,27 +16,61 @@
package com.android.server.wm;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
+import android.annotation.Nullable;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.Property;
+import android.content.res.Resources;
+import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.RoundedCorner;
+import android.view.RoundedCorners;
+import android.view.WindowManager;
import androidx.test.filters.SmallTest;
+import com.android.internal.R;
+
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import org.junit.Before;
@@ -55,14 +89,24 @@
@Presubmit
@RunWith(WindowTestRunner.class)
public class LetterboxUiControllerTest extends WindowTestsBase {
+ private static final int TASKBAR_COLLAPSED_HEIGHT = 10;
+ private static final int TASKBAR_EXPANDED_HEIGHT = 20;
+ private static final int SCREEN_WIDTH = 200;
+ private static final int SCREEN_HEIGHT = 100;
+ private static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0,
+ SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
+ private static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0,
+ SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT);
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
private ActivityRecord mActivity;
+ private Task mTask;
private DisplayContent mDisplayContent;
private LetterboxUiController mController;
private LetterboxConfiguration mLetterboxConfiguration;
+ private final Rect mLetterboxedPortraitTaskBounds = new Rect();
@Before
public void setUp() throws Exception {
@@ -74,6 +118,8 @@
mController = new LetterboxUiController(mWm, mActivity);
}
+ // shouldIgnoreRequestedOrientation
+
@Test
@EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
@@ -134,7 +180,7 @@
}
@Test
- @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
doReturn(false).when(mLetterboxConfiguration)
@@ -143,6 +189,443 @@
assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
+ // shouldRefreshActivityForCameraCompat
+
+ @Test
+ public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() {
+ doReturn(false).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+ assertFalse(mController.shouldRefreshActivityForCameraCompat());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+ public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+ assertFalse(mController.shouldRefreshActivityForCameraCompat());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+ public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldRefreshActivityForCameraCompat());
+ }
+
+ @Test
+ public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldRefreshActivityForCameraCompat());
+ }
+
+ @Test
+ public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldRefreshActivityForCameraCompat());
+ }
+
+ // shouldRefreshActivityViaPauseForCameraCompat
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+ public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() {
+ doReturn(false).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+ assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+ public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+ assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+ public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
+ }
+
+ @Test
+ public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
+ }
+
+ // shouldForceRotateForCameraCompat
+
+ @Test
+ public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() {
+ doReturn(false).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+ assertFalse(mController.shouldForceRotateForCameraCompat());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
+ public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+ assertFalse(mController.shouldForceRotateForCameraCompat());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
+ public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldForceRotateForCameraCompat());
+ }
+
+ @Test
+ public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertFalse(mController.shouldForceRotateForCameraCompat());
+ }
+
+ @Test
+ public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue()
+ throws Exception {
+ doReturn(true).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+ mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertTrue(mController.shouldForceRotateForCameraCompat());
+ }
+
+ @Test
+ public void testGetCropBoundsIfNeeded_noCrop() {
+ final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
+
+ // Do not apply crop if taskbar is collapsed
+ taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
+ assertNull(mController.getExpandedTaskbarOrNull(mainWindow));
+
+ mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4,
+ SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4);
+
+ final Rect noCrop = mController.getCropBoundsIfNeeded(mainWindow);
+ assertNotEquals(null, noCrop);
+ assertEquals(0, noCrop.left);
+ assertEquals(0, noCrop.top);
+ assertEquals(mLetterboxedPortraitTaskBounds.width(), noCrop.right);
+ assertEquals(mLetterboxedPortraitTaskBounds.height(), noCrop.bottom);
+ }
+
+ @Test
+ public void testGetCropBoundsIfNeeded_appliesCrop() {
+ final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
+
+ // Apply crop if taskbar is expanded
+ taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
+ assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow));
+
+ mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
+ SCREEN_HEIGHT);
+
+ final Rect crop = mController.getCropBoundsIfNeeded(mainWindow);
+ assertNotEquals(null, crop);
+ assertEquals(0, crop.left);
+ assertEquals(0, crop.top);
+ assertEquals(mLetterboxedPortraitTaskBounds.width(), crop.right);
+ assertEquals(mLetterboxedPortraitTaskBounds.height() - TASKBAR_EXPANDED_HEIGHT,
+ crop.bottom);
+ }
+
+ @Test
+ public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
+ final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+ final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
+ final float scaling = 2.0f;
+
+ // Apply crop if taskbar is expanded
+ taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
+ assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow));
+ // With SizeCompat scaling
+ doReturn(true).when(mActivity).inSizeCompatMode();
+ mainWindow.mInvGlobalScale = scaling;
+
+ mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
+ SCREEN_HEIGHT);
+
+ final int appWidth = mLetterboxedPortraitTaskBounds.width();
+ final int appHeight = mLetterboxedPortraitTaskBounds.height();
+
+ final Rect crop = mController.getCropBoundsIfNeeded(mainWindow);
+ assertNotEquals(null, crop);
+ assertEquals(0, crop.left);
+ assertEquals(0, crop.top);
+ assertEquals((int) (appWidth * scaling), crop.right);
+ assertEquals((int) ((appHeight - TASKBAR_EXPANDED_HEIGHT) * scaling), crop.bottom);
+ }
+
+ @Test
+ public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() {
+ final float invGlobalScale = 0.5f;
+ final int expectedRadius = 7;
+ final int configurationRadius = 15;
+
+ final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
+ mainWindow.mInvGlobalScale = invGlobalScale;
+ final InsetsState insets = mainWindow.getInsetsState();
+
+ RoundedCorners roundedCorners = new RoundedCorners(
+ /*topLeft=*/ null,
+ /*topRight=*/ null,
+ /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT,
+ configurationRadius, /*centerX=*/ 1, /*centerY=*/ 1),
+ /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT,
+ configurationRadius * 2 /*2 is to test selection of the min radius*/,
+ /*centerX=*/ 1, /*centerY=*/ 1)
+ );
+ doReturn(roundedCorners).when(insets).getRoundedCorners();
+ mLetterboxConfiguration.setLetterboxActivityCornersRadius(-1);
+
+ assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
+ }
+
+ @Test
+ public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() {
+ final float invGlobalScale = 0.5f;
+ final int expectedRadius = 7;
+ final int configurationRadius = 15;
+
+ final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
+ mainWindow.mInvGlobalScale = invGlobalScale;
+ mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+
+ assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
+
+ }
+
+ @Test
+ public void testGetRoundedCornersRadius_noScalingApplied() {
+ final int configurationRadius = 15;
+
+ final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
+ mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+
+ mainWindow.mInvGlobalScale = -1f;
+ assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
+
+ mainWindow.mInvGlobalScale = 0f;
+ assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
+
+ mainWindow.mInvGlobalScale = 1f;
+ assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
+ }
+
+ private WindowState mockForGetCropBoundsAndRoundedCorners(@Nullable InsetsSource taskbar) {
+ final WindowState mainWindow = mock(WindowState.class);
+ final InsetsState insets = mock(InsetsState.class);
+ final Resources resources = mWm.mContext.getResources();
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
+
+ mainWindow.mInvGlobalScale = 1f;
+ spyOn(resources);
+ spyOn(mActivity);
+
+ if (taskbar != null) {
+ taskbar.setVisible(true);
+ doReturn(taskbar).when(insets).peekSource(taskbar.getType());
+ }
+ doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds();
+ doReturn(true).when(mActivity).isVisible();
+ doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+ doReturn(insets).when(mainWindow).getInsetsState();
+ doReturn(attrs).when(mainWindow).getAttrs();
+ doReturn(true).when(mainWindow).isDrawn();
+ doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout();
+ doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed();
+ doReturn(true).when(mLetterboxConfiguration).isLetterboxActivityCornersRounded();
+ doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize(
+ R.dimen.taskbar_frame_height);
+
+ // Need to reinitialise due to the change in resources getDimensionPixelSize output.
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ return mainWindow;
+ }
+
+ // overrideOrientationIfNeeded
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait()
+ throws Exception {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+ public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_NOSENSOR);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+ public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_returnsUnchanged() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+ public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationPortraitOrUndefined_returnsUnchanged() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_PORTRAIT);
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+ public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationLandscape_returnsReverseLandscape() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_LANDSCAPE),
+ SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_returnsUnchanged() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_NOSENSOR), SCREEN_ORIENTATION_NOSENSOR);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_NOSENSOR), SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT), SCREEN_ORIENTATION_NOSENSOR);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_propertyIsFalse_returnsUnchanged()
+ throws Exception {
+ mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ assertEquals(mController.overrideOrientationIfNeeded(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED), SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ // shouldUseDisplayLandscapeNaturalOrientation
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+ public void testShouldUseDisplayLandscapeNaturalOrientation_override_returnsTrue() {
+ prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+ assertTrue(mController.shouldUseDisplayLandscapeNaturalOrientation());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+ public void testShouldUseDisplayLandscapeNaturalOrientation_overrideAndFalseProperty_returnsFalse()
+ throws Exception {
+ mockThatProperty(PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE, /* value */ false);
+
+ mController = new LetterboxUiController(mWm, mActivity);
+
+ prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+ assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+ public void testShouldUseDisplayLandscapeNaturalOrientation_portraitNaturalOrientation_returnsFalse() {
+ prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+ doReturn(ORIENTATION_PORTRAIT).when(mDisplayContent).getNaturalOrientation();
+
+ assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+ public void testShouldUseDisplayLandscapeNaturalOrientation_disabledIgnoreOrientationRequest_returnsFalse() {
+ prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+ mDisplayContent.setIgnoreOrientationRequest(false);
+
+ assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION})
+ public void testShouldUseDisplayLandscapeNaturalOrientation_inMultiWindowMode_returnsFalse() {
+ prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation();
+
+ spyOn(mTask);
+ doReturn(true).when(mTask).inMultiWindowMode();
+
+ assertFalse(mController.shouldUseDisplayLandscapeNaturalOrientation());
+ }
+
private void mockThatProperty(String propertyName, boolean value) throws Exception {
Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
/* className */ "");
@@ -151,6 +634,12 @@
doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
}
+ private void prepareActivityThatShouldUseDisplayLandscapeNaturalOrientation() {
+ spyOn(mDisplayContent);
+ doReturn(ORIENTATION_LANDSCAPE).when(mDisplayContent).getNaturalOrientation();
+ mDisplayContent.setIgnoreOrientationRequest(true);
+ }
+
private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
doReturn(true).when(mLetterboxConfiguration)
.isPolicyForIgnoringRequestedOrientationEnabled();
@@ -160,10 +649,10 @@
private ActivityRecord setUpActivityWithComponent() {
mDisplayContent = new TestDisplayContent
.Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
- Task task = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
+ mTask = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setOnTop(true)
- .setTask(task)
+ .setTask(mTask)
// Set the component to be that of the test class in order to enable compat changes
.setComponent(ComponentName.createRelative(mContext,
com.android.server.wm.LetterboxUiControllerTest.class.getName()))
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index adf694c..db6ac0b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -30,6 +30,7 @@
import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.os.Process.NOBODY_UID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -1220,20 +1221,35 @@
@Test
public void testCreateRecentTaskInfo_detachedTask() {
- final Task task = createTaskBuilder(".Task").setCreateActivity(true).build();
+ final Task task = createTaskBuilder(".Task").build();
+ final ComponentName componentName = getUniqueComponentName();
+ new ActivityBuilder(mSupervisor.mService)
+ .setTask(task)
+ .setUid(NOBODY_UID)
+ .setComponent(componentName)
+ .build();
final TaskDisplayArea tda = task.getDisplayArea();
assertTrue(task.isAttached());
assertTrue(task.supportsMultiWindow());
- RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true);
+ RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertTrue(info.supportsMultiWindow);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ false /* getTasksAllowed */);
+
+ assertFalse(info.topActivity.equals(componentName));
+ assertFalse(info.topActivityInfo.packageName.equals(componentName.getPackageName()));
+ assertFalse(info.baseActivity.equals(componentName));
+
// The task can be put in split screen even if it is not attached now.
task.removeImmediately();
- info = mRecentTasks.createRecentTaskInfo(task, true);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertTrue(info.supportsMultiWindow);
@@ -1242,7 +1258,8 @@
doReturn(false).when(tda).supportsNonResizableMultiWindow();
doReturn(false).when(task).isResizeable();
- info = mRecentTasks.createRecentTaskInfo(task, true);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertFalse(info.supportsMultiWindow);
@@ -1250,7 +1267,8 @@
// the device supports it.
doReturn(true).when(tda).supportsNonResizableMultiWindow();
- info = mRecentTasks.createRecentTaskInfo(task, true);
+ info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */,
+ true /* getTasksAllowed */);
assertTrue(info.supportsMultiWindow);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index 4808474..06e3854 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -468,7 +468,7 @@
mWm.setRecentsAnimationController(mController);
spyOn(mDisplayContent.mFixedRotationTransitionListener);
final ActivityRecord recents = mock(ActivityRecord.class);
- recents.mOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+ recents.setOverrideOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
doReturn(ORIENTATION_PORTRAIT).when(recents)
.getRequestedConfigurationOrientation(anyBoolean());
mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recents);
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 a8e9198..2be193b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -98,7 +98,7 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.server.statusbar.StatusBarManagerInternal;
-import com.android.server.wm.DeviceStateController.FoldState;
+import com.android.server.wm.DeviceStateController.DeviceState;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -197,27 +197,6 @@
}
@Test
- public void testNotApplyStrategyToTranslucentActivitiesWithDifferentUid() {
- mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
- setUpDisplaySizeWithApp(2000, 1000);
- prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
- mActivity.info.setMinAspectRatio(1.2f);
- mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
- // Translucent Activity
- final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
- .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
- .setMinAspectRatio(1.1f)
- .setMaxAspectRatio(3f)
- .build();
- doReturn(false).when(translucentActivity).fillsParent();
- mTask.addChild(translucentActivity);
- // We check bounds
- final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
- final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds();
- assertNotEquals(opaqueBounds, translucentRequestedBounds);
- }
-
- @Test
public void testApplyStrategyToMultipleTranslucentActivities() {
mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
setUpDisplaySizeWithApp(2000, 1000);
@@ -1933,6 +1912,132 @@
}
@Test
+ public void testDisplayAspectRatioForResizablePortraitApps() {
+ // Set up a display in portrait and ignoring orientation request.
+ int displayWidth = 1400;
+ int displayHeight = 1600;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
+
+ // Enable display aspect ratio to take precedence before
+ // fixedOrientationLetterboxAspectRatio
+ mWm.mLetterboxConfiguration
+ .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+ // Set up resizable app in portrait
+ prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, false /* isUnresizable */);
+
+ final TestSplitOrganizer organizer =
+ new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+ // Move activity to split screen which takes half of the screen.
+ mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+ organizer.mPrimary.setBounds(0, 0, displayWidth, getExpectedSplitSize(displayHeight));
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+ // App should launch in fixed orientation letterbox.
+ assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+ // Checking that there is no size compat mode.
+ assertFitted();
+ // Check that the display aspect ratio is used by the app.
+ final float targetMinAspectRatio = 1f * displayHeight / displayWidth;
+ final float delta = 0.01f;
+ assertEquals(targetMinAspectRatio, ActivityRecord
+ .computeAspectRatio(mActivity.getBounds()), delta);
+ }
+
+ @Test
+ public void testDisplayAspectRatioForResizableLandscapeApps() {
+ // Set up a display in landscape and ignoring orientation request.
+ int displayWidth = 1600;
+ int displayHeight = 1400;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
+
+ // Enable display aspect ratio to take precedence before
+ // fixedOrientationLetterboxAspectRatio
+ mWm.mLetterboxConfiguration
+ .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+ // Set up resizable app in landscape
+ prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_LANDSCAPE, false /* isUnresizable */);
+
+ final TestSplitOrganizer organizer =
+ new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+ // Move activity to split screen which takes half of the screen.
+ mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+ organizer.mPrimary.setBounds(0, 0, getExpectedSplitSize(displayWidth), displayHeight);
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+ // App should launch in fixed orientation letterbox.
+ assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+ // Checking that there is no size compat mode.
+ assertFitted();
+ // Check that the display aspect ratio is used by the app.
+ final float targetMinAspectRatio = 1f * displayWidth / displayHeight;
+ final float delta = 0.01f;
+ assertEquals(targetMinAspectRatio, ActivityRecord
+ .computeAspectRatio(mActivity.getBounds()), delta);
+ }
+
+ @Test
+ public void testDisplayAspectRatioForUnresizableLandscapeApps() {
+ // Set up a display in portrait and ignoring orientation request.
+ int displayWidth = 1400;
+ int displayHeight = 1600;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+ // Enable display aspect ratio to take precedence before
+ // fixedOrientationLetterboxAspectRatio
+ mWm.mLetterboxConfiguration
+ .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+ // App should launch in fixed orientation letterbox.
+ assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+ // Checking that there is no size compat mode.
+ assertFitted();
+ // Check that the display aspect ratio is used by the app.
+ final float targetMinAspectRatio = 1f * displayHeight / displayWidth;
+ final float delta = 0.01f;
+ assertEquals(targetMinAspectRatio, ActivityRecord
+ .computeAspectRatio(mActivity.getBounds()), delta);
+ }
+
+ @Test
+ public void testDisplayAspectRatioForUnresizablePortraitApps() {
+ // Set up a display in landscape and ignoring orientation request.
+ int displayWidth = 1600;
+ int displayHeight = 1400;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+ // Enable display aspect ratio to take precedence before
+ // fixedOrientationLetterboxAspectRatio
+ mWm.mLetterboxConfiguration
+ .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
+
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ // App should launch in fixed orientation letterbox.
+ assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+ // Checking that there is no size compat mode.
+ assertFitted();
+ // Check that the display aspect ratio is used by the app.
+ final float targetMinAspectRatio = 1f * displayWidth / displayHeight;
+ final float delta = 0.01f;
+ assertEquals(targetMinAspectRatio, ActivityRecord
+ .computeAspectRatio(mActivity.getBounds()), delta);
+ }
+
+ @Test
public void
testDisplayIgnoreOrientationRequest_orientationLetterboxBecameSizeCompatAfterRotate() {
// Set up a display in landscape and ignoring orientation request.
@@ -2208,6 +2313,29 @@
}
@Test
+ public void testDisplayIgnoreOrientationRequest_disabledViaDeviceConfig_orientationRespected() {
+ // Set up a display in landscape
+ setUpDisplaySizeWithApp(2800, 1400);
+
+ final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+ RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+ spyOn(activity.mWmService.mLetterboxConfiguration);
+ doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
+ .isIgnoreOrientationRequestAllowed();
+
+ // Display should not be rotated.
+ assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+ doReturn(false).when(activity.mWmService.mLetterboxConfiguration)
+ .isIgnoreOrientationRequestAllowed();
+
+ // Display should be rotated.
+ assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+ }
+
+ @Test
public void testSandboxDisplayApis_unresizableAppNotSandboxed() {
// Set up a display in landscape with an unresizable app.
setUpDisplaySizeWithApp(2500, 1000);
@@ -2479,6 +2607,133 @@
}
@Test
+ public void testIsHorizontalReachabilityEnabled_splitScreen_false() {
+ mAtm.mDevEnableNonResizableMultiWindow = true;
+ setUpDisplaySizeWithApp(2800, 1000);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+ final TestSplitOrganizer organizer =
+ new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+ // Unresizable portrait-only activity.
+ prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT);
+
+ // Move activity to split screen which takes half of the screen.
+ mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+ organizer.mPrimary.setBounds(0, 0, 1400, 1000);
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+ // Horizontal reachability is disabled because the app is in split screen.
+ assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+ }
+
+ @Test
+ public void testIsVerticalReachabilityEnabled_splitScreen_false() {
+ mAtm.mDevEnableNonResizableMultiWindow = true;
+ setUpDisplaySizeWithApp(1000, 2800);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ final TestSplitOrganizer organizer =
+ new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+ // Unresizable landscape-only activity.
+ prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE);
+
+ // Move activity to split screen which takes half of the screen.
+ mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+ organizer.mPrimary.setBounds(0, 0, 1000, 1400);
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+ assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+ // Vertical reachability is disabled because the app is in split screen.
+ assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+ }
+
+ @Test
+ public void testIsVerticalReachabilityEnabled_doesNotMatchParentWidth_false() {
+ setUpDisplaySizeWithApp(1000, 2800);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+
+ // Unresizable landscape-only activity.
+ prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE);
+
+ // Rotate to put activity in size compat mode.
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+ // Activity now in size compat mode.
+ assertTrue(mActivity.inSizeCompatMode());
+
+ // Vertical reachability is disabled because the app does not match parent width
+ assertNotEquals(mActivity.getBounds().width(), mActivity.mDisplayContent.getBounds()
+ .width());
+ assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+ }
+
+ @Test
+ public void testIsHorizontalReachabilityEnabled_doesNotMatchParentHeight_false() {
+ setUpDisplaySizeWithApp(2800, 1000);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+
+ // Unresizable portrait-only activity.
+ prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT);
+
+ // Rotate to put activity in size compat mode.
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+ // Activity now in size compat mode.
+ assertTrue(mActivity.inSizeCompatMode());
+
+ // Horizontal reachability is disabled because the app does not match parent height
+ assertNotEquals(mActivity.getBounds().height(), mActivity.mDisplayContent.getBounds()
+ .height());
+ assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+ }
+
+ @Test
+ public void testIsHorizontalReachabilityEnabled_inSizeCompatMode_matchesParentHeight_true() {
+ setUpDisplaySizeWithApp(1800, 2200);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+
+ // Unresizable portrait-only activity.
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ // Rotate to put activity in size compat mode.
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+ // Activity now in size compat mode.
+ assertTrue(mActivity.inSizeCompatMode());
+
+ // Horizontal reachability is enabled because the app matches parent height
+ assertEquals(mActivity.getBounds().height(), mActivity.mDisplayContent.getBounds()
+ .height());
+ assertTrue(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+ }
+
+ @Test
+ public void testIsVerticalReachabilityEnabled_inSizeCompatMode_matchesParentWidth_true() {
+ setUpDisplaySizeWithApp(2200, 1800);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+
+ // Unresizable landscape-only activity.
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+ // Rotate to put activity in size compat mode.
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+ // Activity now in size compat mode.
+ assertTrue(mActivity.inSizeCompatMode());
+
+ // Vertical reachability is enabled because the app matches parent width
+ assertEquals(mActivity.getBounds().width(), mActivity.mDisplayContent.getBounds().width());
+ assertTrue(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+ }
+
+ @Test
public void testLetterboxDetailsForStatusBar_noLetterbox() {
setUpDisplaySizeWithApp(2800, 1000);
addStatusBar(mActivity.mDisplayContent);
@@ -2589,7 +2844,7 @@
mActivity.mRootWindowContainer.performSurfacePlacement();
final ArgumentCaptor<Rect> cropCapturer = ArgumentCaptor.forClass(Rect.class);
- verify(mTransaction, times(2)).setWindowCrop(
+ verify(mTransaction, times(2)).setCrop(
eq(w1.getSurfaceControl()),
cropCapturer.capture()
);
@@ -2678,6 +2933,39 @@
}
@Test
+ public void testUpdateResolvedBoundsHorizontalPosition_leftInsets_appCentered() {
+ // Set up folded display
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1100, 2100)
+ .setCanRotate(true)
+ .build();
+ display.setIgnoreOrientationRequest(true);
+ final DisplayPolicy policy = display.getDisplayPolicy();
+ DisplayPolicy.DecorInsets.Info decorInfo = policy.getDecorInsetsInfo(ROTATION_90,
+ display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+ decorInfo.mNonDecorInsets.set(130, 0, 60, 0);
+ spyOn(policy);
+ doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90,
+ display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+ mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+
+ setUpApp(display);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ // Resize the display to simulate unfolding in portrait
+ resizeDisplay(mTask.mDisplayContent, 2200, 1800);
+ assertTrue(mActivity.inSizeCompatMode());
+
+ // Simulate real display not taking non-decor insets into consideration
+ display.getWindowConfiguration().setAppBounds(0, 0, 2200, 1800);
+
+ // Rotate display to landscape
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+ // App is centered
+ assertEquals(mActivity.getBounds(), new Rect(350, 50, 1450, 2150));
+ }
+
+ @Test
public void testUpdateResolvedBoundsHorizontalPosition_left() {
// Display configured as (2800, 1400).
assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity(
@@ -2823,6 +3111,20 @@
}
@Test
+ public void testApplyAspectRatio_containingRatioAlmostEqualToMaxRatio_boundsUnchanged() {
+ setUpDisplaySizeWithApp(1981, 2576);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+
+ final Rect originalBounds = new Rect(mActivity.getBounds());
+ prepareUnresizable(mActivity, 1.3f, SCREEN_ORIENTATION_UNSPECIFIED);
+
+ // The containing aspect ratio is now 1.3003534, while the desired aspect ratio is 1.3. The
+ // bounds of the activity should not be changed as the difference is too small
+ assertEquals(mActivity.getBounds(), originalBounds);
+ }
+
+ @Test
public void testUpdateResolvedBoundsHorizontalPosition_activityFillParentWidth() {
// When activity width equals parent width, multiplier shouldn't have any effect.
assertHorizontalPositionForDifferentDisplayConfigsForLandscapeActivity(
@@ -2834,6 +3136,39 @@
}
@Test
+ public void testUpdateResolvedBoundsVerticalPosition_topInsets_appCentered() {
+ // Set up folded display
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, 2100, 1100)
+ .setCanRotate(true)
+ .build();
+ display.setIgnoreOrientationRequest(true);
+ final DisplayPolicy policy = display.getDisplayPolicy();
+ DisplayPolicy.DecorInsets.Info decorInfo = policy.getDecorInsetsInfo(ROTATION_90,
+ display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+ decorInfo.mNonDecorInsets.set(0, 130, 0, 60);
+ spyOn(policy);
+ doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90,
+ display.mBaseDisplayHeight, display.mBaseDisplayWidth);
+ mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+
+ setUpApp(display);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+ // Resize the display to simulate unfolding in portrait
+ resizeDisplay(mTask.mDisplayContent, 1800, 2200);
+ assertTrue(mActivity.inSizeCompatMode());
+
+ // Simulate real display not taking non-decor insets into consideration
+ display.getWindowConfiguration().setAppBounds(0, 0, 1800, 2200);
+
+ // Rotate display to landscape
+ rotateDisplay(mActivity.mDisplayContent, ROTATION_90);
+
+ // App is centered
+ assertEquals(mActivity.getBounds(), new Rect(50, 350, 2150, 1450));
+ }
+
+ @Test
public void testUpdateResolvedBoundsVerticalPosition_top() {
// Display configured as (1400, 2800).
assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
@@ -2954,9 +3289,9 @@
private void setFoldablePosture(boolean isHalfFolded, boolean isTabletop) {
final DisplayRotation r = mActivity.mDisplayContent.getDisplayRotation();
doReturn(isHalfFolded).when(r).isDisplaySeparatingHinge();
- doReturn(false).when(r).isDeviceInPosture(any(FoldState.class), anyBoolean());
+ doReturn(false).when(r).isDeviceInPosture(any(DeviceState.class), anyBoolean());
if (isHalfFolded) {
- doReturn(true).when(r).isDeviceInPosture(FoldState.HALF_FOLDED, isTabletop);
+ doReturn(true).when(r).isDeviceInPosture(DeviceState.HALF_FOLDED, isTabletop);
}
mActivity.recomputeConfiguration();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 2420efc..8244f94 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -796,6 +796,72 @@
}
@Test
+ public void testApplyTransaction_createTaskFragment_overrideBounds() {
+ final Task task = createTask(mDisplayContent);
+ final ActivityRecord activityAtBottom = createActivityRecord(task);
+ final int uid = Binder.getCallingUid();
+ activityAtBottom.info.applicationInfo.uid = uid;
+ activityAtBottom.getTask().effectiveUid = uid;
+ mTaskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .setFragmentToken(mFragmentToken)
+ .createActivityCount(1)
+ .build();
+ mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+ final IBinder fragmentToken1 = new Binder();
+ final Rect bounds = new Rect(100, 100, 500, 1000);
+ final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder(
+ mOrganizerToken, fragmentToken1, activityAtBottom.token)
+ .setPairedActivityToken(activityAtBottom.token)
+ .setInitialBounds(bounds)
+ .build();
+ mTransaction.setTaskFragmentOrganizer(mIOrganizer);
+ mTransaction.createTaskFragment(params);
+ assertApplyTransactionAllowed(mTransaction);
+
+ // Successfully created a TaskFragment.
+ final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(
+ fragmentToken1);
+ assertNotNull(taskFragment);
+ // The relative embedded bounds is updated to the initial requested bounds.
+ assertEquals(bounds, taskFragment.getRelativeEmbeddedBounds());
+ }
+
+ @Test
+ public void testApplyTransaction_createTaskFragment_withPairedActivityToken() {
+ final Task task = createTask(mDisplayContent);
+ final ActivityRecord activityAtBottom = createActivityRecord(task);
+ final int uid = Binder.getCallingUid();
+ activityAtBottom.info.applicationInfo.uid = uid;
+ activityAtBottom.getTask().effectiveUid = uid;
+ mTaskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .setFragmentToken(mFragmentToken)
+ .createActivityCount(1)
+ .build();
+ mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment);
+ final IBinder fragmentToken1 = new Binder();
+ final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder(
+ mOrganizerToken, fragmentToken1, activityAtBottom.token)
+ .setPairedActivityToken(activityAtBottom.token)
+ .build();
+ mTransaction.setTaskFragmentOrganizer(mIOrganizer);
+ mTransaction.createTaskFragment(params);
+ assertApplyTransactionAllowed(mTransaction);
+
+ // Successfully created a TaskFragment.
+ final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(
+ fragmentToken1);
+ assertNotNull(taskFragment);
+ // The new TaskFragment should be positioned right above the paired activity.
+ assertEquals(task.mChildren.indexOf(activityAtBottom) + 1,
+ task.mChildren.indexOf(taskFragment));
+ // The top TaskFragment should remain on top.
+ assertEquals(task.mChildren.indexOf(taskFragment) + 1,
+ task.mChildren.indexOf(mTaskFragment));
+ }
+
+ @Test
public void testApplyTransaction_enforceHierarchyChange_reparentChildren() {
doReturn(true).when(mTaskFragment).isAttached();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index bf1d1fa..83be4f0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
+import static android.view.Surface.ROTATION_0;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -78,6 +79,12 @@
final InputMonitor inputMonitor = getInputMonitor();
spyOn(inputMonitor);
doNothing().when(inputMonitor).resumeDispatchingLw(any());
+
+ // For devices that set the sysprop ro.bootanim.set_orientation_<display_id>
+ // See DisplayRotation#readDefaultDisplayRotation for context.
+ // Without that, meaning of height and width in context of the tests can be swapped if
+ // the default rotation is 90 or 270.
+ displayRotation.setRotation(ROTATION_0);
}
public static class Builder {
@@ -203,6 +210,7 @@
}
final int displayId = SystemServicesTestRule.sNextDisplayId++;
+ mInfo.displayId = displayId;
final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
mInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
final TestDisplayContent newDisplay = createInternal(display);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 06a79f4..1407cdd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -391,7 +391,7 @@
dc.updateOrientation();
dc.sendNewConfiguration();
spyOn(wallpaperWindow);
- doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getLastReportedBounds();
+ doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getParentFrame();
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index ed7d123..2446fc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -1557,7 +1557,7 @@
@Override
int getOrientation() {
- return getOrientation(super.mOrientation);
+ return getOrientation(super.getOverrideOrientation());
}
@Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 0568f2a..514aec1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -42,6 +42,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
@@ -544,7 +545,12 @@
win.applyWithNextDraw(t -> handledT[0] = t);
assertTrue(win.useBLASTSync());
final SurfaceControl.Transaction drawT = new StubTransaction();
+ final SurfaceControl.Transaction currT = win.getSyncTransaction();
+ clearInvocations(currT);
+ win.mWinAnimator.mLastHidden = true;
assertTrue(win.finishDrawing(drawT, Integer.MAX_VALUE));
+ // The draw transaction should be merged to current transaction even if the state is hidden.
+ verify(currT).merge(eq(drawT));
assertEquals(drawT, handledT[0]);
assertFalse(win.useBLASTSync());
@@ -969,6 +975,19 @@
assertFalse(sameTokenWindow.needsRelativeLayeringToIme());
}
+ @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD})
+ @Test
+ public void testNeedsRelativeLayeringToIme_systemDialog() {
+ WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
+ mDisplayContent,
+ "SystemDialog", true);
+ mDisplayContent.setImeLayeringTarget(mAppWindow);
+ mAppWindow.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ makeWindowVisible(mImeWindow);
+ systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
+ assertTrue(systemDialogWindow.needsRelativeLayeringToIme());
+ }
+
@Test
public void testSetFreezeInsetsState() {
final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
@@ -1119,7 +1138,9 @@
spyOn(app.getDisplayContent());
app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- verify(app.getDisplayContent()).updateImeControlTarget();
+ // Expect updateImeParent will be invoked when the configuration of the IME control
+ // target has changed.
+ verify(app.getDisplayContent()).updateImeControlTarget(eq(true) /* updateImeParent */);
assertEquals(mAppWindow, mDisplayContent.getImeTarget(IME_TARGET_CONTROL).getWindow());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 268aa3e..f8b8094 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -253,6 +253,12 @@
// device form factors.
mAtm.mWindowManager.mLetterboxConfiguration
.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(false);
+ // Ensure aspect ratio for al apps isn't overridden on any device target.
+ // {@link com.android.internal.R.bool
+ // .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled}, may be set on
+ // some device form factors.
+ mAtm.mWindowManager.mLetterboxConfiguration
+ .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
checkDeviceSpecificOverridesNotApplied();
}
@@ -267,6 +273,8 @@
mAtm.mWindowManager.mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
mAtm.mWindowManager.mLetterboxConfiguration
.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ mAtm.mWindowManager.mLetterboxConfiguration
+ .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 77fca45..7959d82 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -22,6 +22,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
@@ -31,6 +32,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
@@ -543,4 +545,28 @@
assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(),
mDisplayContent.getImeContainer().getSurfaceControl());
}
+
+ @Test
+ public void testSystemDialogWindow_expectHigherThanIme_inMultiWindow() {
+ // Simulate the app window is in multi windowing mode and being IME target
+ mAppWindow.getConfiguration().windowConfiguration.setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW);
+ mDisplayContent.setImeLayeringTarget(mAppWindow);
+ mDisplayContent.setImeInputTarget(mAppWindow);
+ makeWindowVisible(mImeWindow);
+
+ // Create a popupWindow
+ final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
+ mDisplayContent, "SystemDialog", true);
+ systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
+ spyOn(systemDialogWindow);
+
+ mDisplayContent.assignChildLayers(mTransaction);
+
+ // Verify the surface layer of the popupWindow should higher than IME
+ verify(systemDialogWindow).needsRelativeLayeringToIme();
+ assertThat(systemDialogWindow.needsRelativeLayeringToIme()).isTrue();
+ assertZOrderGreaterThan(mTransaction, systemDialogWindow.getSurfaceControl(),
+ mDisplayContent.getImeContainer().getSurfaceControl());
+ }
}
diff --git a/services/usb/java/com/android/server/usb/UsbAlsaManager.java b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
index 42a5af7..17c354a 100644
--- a/services/usb/java/com/android/server/usb/UsbAlsaManager.java
+++ b/services/usb/java/com/android/server/usb/UsbAlsaManager.java
@@ -36,6 +36,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
/**
@@ -106,6 +107,12 @@
return false;
}
+ /**
+ * List of connected MIDI devices
+ */
+ private final HashMap<String, UsbMidiDevice>
+ mMidiDevices = new HashMap<String, UsbMidiDevice>();
+
// UsbMidiDevice for USB peripheral mode (gadget) device
private UsbMidiDevice mPeripheralMidiDevice = null;
@@ -249,6 +256,8 @@
}
}
+ addMidiDevice(deviceAddress, usbDevice, parser, cardRec);
+
logDevices("deviceAdded()");
if (DEBUG) {
@@ -256,6 +265,54 @@
}
}
+ private void addMidiDevice(String deviceAddress, UsbDevice usbDevice,
+ UsbDescriptorParser parser, AlsaCardsParser.AlsaCardRecord cardRec) {
+ boolean hasMidi = parser.hasMIDIInterface();
+ // UsbHostManager will create UsbDirectMidiDevices instead if MIDI 2 is supported.
+ boolean hasMidi2 = parser.containsUniversalMidiDeviceEndpoint();
+ if (DEBUG) {
+ Slog.d(TAG, "hasMidi: " + hasMidi + " mHasMidiFeature:" + mHasMidiFeature);
+ Slog.d(TAG, "hasMidi2: " + hasMidi2);
+ }
+ if (mHasMidiFeature && hasMidi && !hasMidi2) {
+ Bundle properties = new Bundle();
+ String manufacturer = usbDevice.getManufacturerName();
+ String product = usbDevice.getProductName();
+ String version = usbDevice.getVersion();
+ String name;
+ if (manufacturer == null || manufacturer.isEmpty()) {
+ name = product;
+ } else if (product == null || product.isEmpty()) {
+ name = manufacturer;
+ } else {
+ name = manufacturer + " " + product;
+ }
+ properties.putString(MidiDeviceInfo.PROPERTY_NAME, name);
+ properties.putString(MidiDeviceInfo.PROPERTY_MANUFACTURER, manufacturer);
+ properties.putString(MidiDeviceInfo.PROPERTY_PRODUCT, product);
+ properties.putString(MidiDeviceInfo.PROPERTY_VERSION, version);
+ properties.putString(MidiDeviceInfo.PROPERTY_SERIAL_NUMBER,
+ usbDevice.getSerialNumber());
+ properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_CARD, cardRec.getCardNum());
+ properties.putInt(MidiDeviceInfo.PROPERTY_ALSA_DEVICE, 0 /*deviceNum*/);
+ properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, usbDevice);
+
+ int numLegacyMidiInputs = parser.calculateNumLegacyMidiInputs();
+ int numLegacyMidiOutputs = parser.calculateNumLegacyMidiOutputs();
+ if (DEBUG) {
+ Slog.d(TAG, "numLegacyMidiInputs: " + numLegacyMidiInputs);
+ Slog.d(TAG, "numLegacyMidiOutputs:" + numLegacyMidiOutputs);
+ }
+
+ UsbMidiDevice usbMidiDevice = UsbMidiDevice.create(mContext, properties,
+ cardRec.getCardNum(), 0 /*device*/, numLegacyMidiInputs,
+ numLegacyMidiOutputs);
+ if (usbMidiDevice != null) {
+ mMidiDevices.put(deviceAddress, usbMidiDevice);
+ }
+ }
+ }
+
/* package */ synchronized void usbDeviceRemoved(String deviceAddress/*UsbDevice usbDevice*/) {
if (DEBUG) {
Slog.d(TAG, "deviceRemoved(" + deviceAddress + ")");
@@ -269,6 +326,13 @@
selectDefaultDevice(); // if there any external devices left, select one of them
}
+ // MIDI
+ UsbMidiDevice usbMidiDevice = mMidiDevices.remove(deviceAddress);
+ if (usbMidiDevice != null) {
+ Slog.i(TAG, "USB MIDI Device Removed: " + deviceAddress);
+ IoUtils.closeQuietly(usbMidiDevice);
+ }
+
logDevices("usbDeviceRemoved()");
}
@@ -324,6 +388,12 @@
usbAlsaDevice.dump(dump, "alsa_devices", UsbAlsaManagerProto.ALSA_DEVICES);
}
+ for (String deviceAddr : mMidiDevices.keySet()) {
+ // A UsbMidiDevice does not have a handle to the UsbDevice anymore
+ mMidiDevices.get(deviceAddr).dump(deviceAddr, dump, "midi_devices",
+ UsbAlsaManagerProto.MIDI_DEVICES);
+ }
+
dump.end(token);
}
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 1b92699..e256f54 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -535,7 +535,6 @@
private boolean mInHostModeWithNoAccessoryConnected;
private boolean mSourcePower;
private boolean mSinkPower;
- private boolean mConfigured;
private boolean mAudioAccessoryConnected;
private boolean mAudioAccessorySupported;
@@ -568,7 +567,12 @@
private final UsbPermissionManager mPermissionManager;
private NotificationManager mNotificationManager;
+ /**
+ * Do not debounce for the first disconnect after resetUsbGadget.
+ */
+ protected boolean mResetUsbGadgetDisableDebounce;
protected boolean mConnected;
+ protected boolean mConfigured;
protected long mScreenUnlockedFunctions;
protected boolean mBootCompleted;
protected boolean mCurrentFunctionsApplied;
@@ -713,15 +717,29 @@
Slog.e(TAG, "unknown state " + state);
return;
}
- if (configured == 0) removeMessages(MSG_UPDATE_STATE);
if (connected == 1) removeMessages(MSG_FUNCTION_SWITCH_TIMEOUT);
Message msg = Message.obtain(this, MSG_UPDATE_STATE);
msg.arg1 = connected;
msg.arg2 = configured;
- // debounce disconnects to avoid problems bringing up USB tethering
- sendMessageDelayed(msg,
+ if (DEBUG) {
+ Slog.i(TAG, "mResetUsbGadgetDisableDebounce:" + mResetUsbGadgetDisableDebounce
+ + " connected:" + connected + "configured:" + configured);
+ }
+ if (mResetUsbGadgetDisableDebounce) {
+ // Do not debounce disconnect after resetUsbGadget.
+ sendMessage(msg);
+ if (connected == 1) mResetUsbGadgetDisableDebounce = false;
+ } else {
+ if (configured == 0) {
+ removeMessages(MSG_UPDATE_STATE);
+ if (DEBUG) Slog.i(TAG, "removeMessages MSG_UPDATE_STATE");
+ }
+ if (connected == 1) removeMessages(MSG_FUNCTION_SWITCH_TIMEOUT);
+ // debounce disconnects to avoid problems bringing up USB tethering.
+ sendMessageDelayed(msg,
(connected == 0) ? (mScreenLocked ? DEVICE_STATE_UPDATE_DELAY
: DEVICE_STATE_UPDATE_DELAY_EXT) : 0);
+ }
}
public void updateHostState(UsbPort port, UsbPortStatus status) {
@@ -971,7 +989,10 @@
int operationId = sUsbOperationCount.incrementAndGet();
mConnected = (msg.arg1 == 1);
mConfigured = (msg.arg2 == 1);
-
+ if (DEBUG) {
+ Slog.i(TAG, "handleMessage MSG_UPDATE_STATE " + "mConnected:" + mConnected
+ + " mConfigured:" + mConfigured);
+ }
updateUsbNotification(false);
updateAdbNotification(false);
if (mBootCompleted) {
@@ -2116,9 +2137,16 @@
}
try {
+ // MSG_ACCESSORY_MODE_ENTER_TIMEOUT has to be removed to allow exiting
+ // AOAP mode during resetUsbGadget.
+ removeMessages(MSG_ACCESSORY_MODE_ENTER_TIMEOUT);
+ if (mConfigured) {
+ mResetUsbGadgetDisableDebounce = true;
+ }
mUsbGadgetHal.reset();
} catch (Exception e) {
Slog.e(TAG, "reset Usb Gadget failed", e);
+ mResetUsbGadgetDisableDebounce = false;
}
}
break;
diff --git a/services/usb/java/com/android/server/usb/UsbHostManager.java b/services/usb/java/com/android/server/usb/UsbHostManager.java
index f389276..b3eb285 100644
--- a/services/usb/java/com/android/server/usb/UsbHostManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHostManager.java
@@ -444,14 +444,19 @@
} else {
Slog.e(TAG, "Universal Midi Device is null.");
}
- }
- if (parser.containsLegacyMidiDeviceEndpoint()) {
- UsbDirectMidiDevice midiDevice = UsbDirectMidiDevice.create(mContext,
- newDevice, parser, false, uniqueUsbDeviceIdentifier);
- if (midiDevice != null) {
- midiDevices.add(midiDevice);
- } else {
- Slog.e(TAG, "Legacy Midi Device is null.");
+
+ // Use UsbDirectMidiDevice only if this supports MIDI 2.0 as well.
+ // ALSA removes the audio sound card if MIDI interfaces are removed.
+ // This means that as long as ALSA is used for audio, MIDI 1.0 USB
+ // devices should use the ALSA path for MIDI.
+ if (parser.containsLegacyMidiDeviceEndpoint()) {
+ midiDevice = UsbDirectMidiDevice.create(mContext,
+ newDevice, parser, false, uniqueUsbDeviceIdentifier);
+ if (midiDevice != null) {
+ midiDevices.add(midiDevice);
+ } else {
+ Slog.e(TAG, "Legacy Midi Device is null.");
+ }
}
}
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java
index 3f2d8c8..c6ea228 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbConfigDescriptor.java
@@ -79,6 +79,10 @@
mInterfaceDescriptors.add(interfaceDesc);
}
+ ArrayList<UsbInterfaceDescriptor> getInterfaceDescriptors() {
+ return mInterfaceDescriptors;
+ }
+
private boolean isAudioInterface(UsbInterfaceDescriptor descriptor) {
return descriptor.getUsbClass() == UsbDescriptor.CLASSID_AUDIO
&& descriptor.getUsbSubclass() == UsbDescriptor.AUDIO_AUDIOSTREAMING;
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
index cd6ea68..626ce89 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbDescriptorParser.java
@@ -40,6 +40,7 @@
private UsbDeviceDescriptor mDeviceDescriptor;
private UsbConfigDescriptor mCurConfigDescriptor;
private UsbInterfaceDescriptor mCurInterfaceDescriptor;
+ private UsbEndpointDescriptor mCurEndpointDescriptor;
// The AudioClass spec implemented by the AudioClass Interfaces
// This may well be different than the overall USB Spec.
@@ -165,7 +166,7 @@
break;
case UsbDescriptor.DESCRIPTORTYPE_ENDPOINT:
- descriptor = new UsbEndpointDescriptor(length, type);
+ descriptor = mCurEndpointDescriptor = new UsbEndpointDescriptor(length, type);
if (mCurInterfaceDescriptor != null) {
mCurInterfaceDescriptor.addEndpointDescriptor(
(UsbEndpointDescriptor) descriptor);
@@ -265,6 +266,9 @@
+ Integer.toHexString(subClass));
break;
}
+ if (mCurEndpointDescriptor != null && descriptor != null) {
+ mCurEndpointDescriptor.setClassSpecificEndpointDescriptor(descriptor);
+ }
}
break;
@@ -798,6 +802,84 @@
/**
* @hide
*/
+ private int calculateNumLegacyMidiPorts(boolean isOutput) {
+ // Only look at the first config.
+ UsbConfigDescriptor configDescriptor = null;
+ for (UsbDescriptor descriptor : mDescriptors) {
+ if (descriptor.getType() == UsbDescriptor.DESCRIPTORTYPE_CONFIG) {
+ if (descriptor instanceof UsbConfigDescriptor) {
+ configDescriptor = (UsbConfigDescriptor) descriptor;
+ break;
+ } else {
+ Log.w(TAG, "Unrecognized Config l: " + descriptor.getLength()
+ + " t:0x" + Integer.toHexString(descriptor.getType()));
+ }
+ }
+ }
+ if (configDescriptor == null) {
+ Log.w(TAG, "Config not found");
+ return 0;
+ }
+
+ ArrayList<UsbInterfaceDescriptor> legacyMidiInterfaceDescriptors =
+ new ArrayList<UsbInterfaceDescriptor>();
+ for (UsbInterfaceDescriptor interfaceDescriptor
+ : configDescriptor.getInterfaceDescriptors()) {
+ if (interfaceDescriptor.getUsbClass() == UsbDescriptor.CLASSID_AUDIO) {
+ if (interfaceDescriptor.getUsbSubclass() == UsbDescriptor.AUDIO_MIDISTREAMING) {
+ UsbDescriptor midiHeaderDescriptor =
+ interfaceDescriptor.getMidiHeaderInterfaceDescriptor();
+ if (midiHeaderDescriptor != null) {
+ if (midiHeaderDescriptor instanceof UsbMSMidiHeader) {
+ UsbMSMidiHeader midiHeader =
+ (UsbMSMidiHeader) midiHeaderDescriptor;
+ if (midiHeader.getMidiStreamingClass() == MS_MIDI_1_0) {
+ legacyMidiInterfaceDescriptors.add(interfaceDescriptor);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ int count = 0;
+ for (UsbInterfaceDescriptor interfaceDescriptor : legacyMidiInterfaceDescriptors) {
+ for (int i = 0; i < interfaceDescriptor.getNumEndpoints(); i++) {
+ UsbEndpointDescriptor endpoint =
+ interfaceDescriptor.getEndpointDescriptor(i);
+ // 0 is output, 1 << 7 is input.
+ if ((endpoint.getDirection() == 0) == isOutput) {
+ UsbDescriptor classSpecificEndpointDescriptor =
+ endpoint.getClassSpecificEndpointDescriptor();
+ if (classSpecificEndpointDescriptor != null
+ && (classSpecificEndpointDescriptor instanceof UsbACMidi10Endpoint)) {
+ UsbACMidi10Endpoint midiEndpoint =
+ (UsbACMidi10Endpoint) classSpecificEndpointDescriptor;
+ count += midiEndpoint.getNumJacks();
+ }
+ }
+ }
+ }
+ return count;
+ }
+
+ /**
+ * @hide
+ */
+ public int calculateNumLegacyMidiInputs() {
+ return calculateNumLegacyMidiPorts(false /*isOutput*/);
+ }
+
+ /**
+ * @hide
+ */
+ public int calculateNumLegacyMidiOutputs() {
+ return calculateNumLegacyMidiPorts(true /*isOutput*/);
+ }
+
+ /**
+ * @hide
+ */
public float getInputHeadsetProbability() {
if (hasMIDIInterface()) {
return 0.0f;
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
index ab07ce7..1f448ac 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
@@ -79,6 +79,8 @@
private byte mRefresh;
private byte mSyncAddress;
+ private UsbDescriptor mClassSpecificEndpointDescriptor;
+
public UsbEndpointDescriptor(int length, byte type) {
super(length, type);
mHierarchyLevel = 4;
@@ -112,6 +114,14 @@
return mEndpointAddress & UsbEndpointDescriptor.MASK_ENDPOINT_DIRECTION;
}
+ void setClassSpecificEndpointDescriptor(UsbDescriptor descriptor) {
+ mClassSpecificEndpointDescriptor = descriptor;
+ }
+
+ UsbDescriptor getClassSpecificEndpointDescriptor() {
+ return mClassSpecificEndpointDescriptor;
+ }
+
/**
* Returns a UsbEndpoint that this UsbEndpointDescriptor is describing.
*/
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index d0a536b..04b08d4 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -55,6 +55,7 @@
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1152,6 +1153,11 @@
Slog.w(TAG, "Failed to report onError status: " + e);
}
}
+ // Can improve to log exit reason if needed
+ HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+ mDetectorType,
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH,
+ mVoiceInteractionServiceUid);
}
@Override
diff --git a/telephony/java/android/telephony/NetworkScanRequest.java b/telephony/java/android/telephony/NetworkScanRequest.java
index 326f417..65c2146 100644
--- a/telephony/java/android/telephony/NetworkScanRequest.java
+++ b/telephony/java/android/telephony/NetworkScanRequest.java
@@ -26,7 +26,7 @@
import java.util.Arrays;
/**
- * Defines a request to peform a network scan.
+ * Defines a request to perform a network scan.
*
* This class defines whether the network scan will be performed only once or periodically until
* cancelled, when the scan is performed periodically, the time interval is not controlled by the
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index aacc17a4..3361502 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -113,4 +113,18 @@
}
return false
}
+
+ fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
+ val button = uiDevice.wait(Until.findObject(By.res(getPackage(),
+ "toggle_fixed_portrait_btn")), FIND_TIMEOUT)
+ require(button != null) {
+ "Button not found, this usually happens when the device " +
+ "was left in an unknown state (e.g. Screen turned off)"
+ }
+ button.click()
+ mInstrumentation.waitForIdleSync()
+ // Ensure app relaunching transition finish and the IME has shown
+ wmHelper.waitForAppTransitionIdle()
+ wmHelper.waitImeShown()
+ }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
new file mode 100644
index 0000000..3b3bce6
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
+import android.view.Surface
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group2
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
+import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.traces.region.RegionSubject
+import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test IME window shown on the app with fixing portrait orientation.
+ * To run this test: `atest FlickerTests:OpenImeWindowToFixedPortraitAppTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group2
+class OpenImeWindowToFixedPortraitAppTest (private val testSpec: FlickerTestParameter) {
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
+
+ @FlickerBuilderProvider
+ fun buildFlicker(): FlickerBuilder {
+ return FlickerBuilder(instrumentation).apply {
+ setup {
+ eachRun {
+ testApp.launchViaIntent(wmHelper)
+ testApp.openIME(device, wmHelper)
+ // Enable letterbox when the app calls setRequestedOrientation
+ device.executeShellCommand("cmd window set-ignore-orientation-request true")
+ }
+ }
+ transitions {
+ testApp.toggleFixPortraitOrientation(wmHelper)
+ }
+ teardown {
+ eachRun {
+ testApp.exit()
+ device.executeShellCommand("cmd window set-ignore-orientation-request false")
+ }
+ }
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun imeLayerVisibleStart() {
+ testSpec.assertLayersStart {
+ this.isVisible(FlickerComponentName.IME)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun imeLayerExistsEnd() {
+ testSpec.assertLayersEnd {
+ this.isVisible(FlickerComponentName.IME)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun imeLayerVisibleRegionKeepsTheSame() {
+ var imeLayerVisibleRegionBeforeTransition: RegionSubject? = null
+ testSpec.assertLayersStart {
+ imeLayerVisibleRegionBeforeTransition = this.visibleRegion(FlickerComponentName.IME)
+ }
+ testSpec.assertLayersEnd {
+ this.visibleRegion(FlickerComponentName.IME)
+ .coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ fun appWindowWithLetterboxCoversExactlyOnScreen() {
+ val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
+ testSpec.assertLayersEnd {
+ this.visibleRegion(testApp.component, FlickerComponentName.LETTERBOX)
+ .coversExactly(displayBounds)
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance()
+ .getConfigNonRotationTests(
+ supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270),
+ supportedNavigationModes = listOf(
+ WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
+ WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index b8ef195..efd80f2 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -45,7 +45,7 @@
android:theme="@style/CutoutShortEdges"
android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus"
android:windowSoftInputMode="stateVisible"
- android:configChanges="orientation|screenSize"
+ android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="ImeAppAutoFocus"
android:exported="true">
<intent-filter>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index baaf707..e71fe80 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -26,14 +26,27 @@
android:layout_width="match_parent"
android:imeOptions="flagNoExtractUi"
android:inputType="text"/>
- <Button
- android:id="@+id/finish_activity_btn"
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Finish activity" />
- <Button
- android:id="@+id/start_dialog_themed_activity_btn"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Start dialog themed activity" />
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/finish_activity_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Finish activity" />
+ <Button
+ android:id="@+id/start_dialog_themed_activity_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Dialog activity" />
+ <ToggleButton
+ android:id="@+id/toggle_fixed_portrait_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOn="Portrait (On)"
+ android:textOff="Portrait (Off)"
+ />
+ </LinearLayout>
</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
index bb200f1..7ee8deb 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
@@ -16,21 +16,29 @@
package com.android.server.wm.flicker.testapp;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
import android.content.Intent;
import android.widget.Button;
import android.widget.EditText;
+import android.widget.ToggleButton;
public class ImeActivityAutoFocus extends ImeActivity {
-
@Override
protected void onStart() {
super.onStart();
- EditText editTextField = findViewById(R.id.plain_text_input);
- editTextField.requestFocus();
-
Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
startThemedActivityButton.setOnClickListener(
button -> startActivity(new Intent(this, DialogThemedActivity.class)));
+
+ ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
+ toggleFixedPortraitButton.setOnCheckedChangeListener(
+ (button, isChecked) -> setRequestedOrientation(
+ isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
+
+ EditText editTextField = findViewById(R.id.plain_text_input);
+ editTextField.requestFocus();
}
}