diff options
210 files changed, 6282 insertions, 2800 deletions
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 746b8f70b6af..0b4862176040 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -24,6 +24,7 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationHistory; import android.app.NotificationManager; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ParceledListSlice; @@ -225,6 +226,7 @@ interface INotificationManager void setNotificationDelegate(String callingPkg, String delegate); String getNotificationDelegate(String callingPkg); boolean canNotifyAsPackage(String callingPkg, String targetPkg, int userId); + boolean canUseFullScreenIntent(in AttributionSource attributionSource); void setPrivateNotificationsAllowed(boolean allow); boolean getPrivateNotificationsAllowed(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index f80373912dcb..63da0a231286 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -7016,6 +7016,22 @@ public class Notification implements Parcelable } /** + * @return true for custom notifications, including notifications + * with DecoratedCustomViewStyle or DecoratedMediaCustomViewStyle, + * and other notifications with user-provided custom views. + * + * @hide + */ + public Boolean isCustomNotification() { + if (contentView == null + && bigContentView == null + && headsUpContentView == null) { + return false; + } + return true; + } + + /** * @return true if this notification is showing as a bubble * * @hide diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 80f64e03afe8..785470f2f22e 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -31,7 +31,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.PermissionChecker; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.graphics.drawable.Icon; @@ -877,19 +876,11 @@ public class NotificationManager { * {@link android.provider.Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT}. */ public boolean canUseFullScreenIntent() { - final int result = PermissionChecker.checkPermissionForPreflight(mContext, - android.Manifest.permission.USE_FULL_SCREEN_INTENT, - mContext.getAttributionSource()); - - switch (result) { - case PermissionChecker.PERMISSION_GRANTED: - return true; - case PermissionChecker.PERMISSION_SOFT_DENIED: - case PermissionChecker.PERMISSION_HARD_DENIED: - return false; - default: - if (localLOGV) Log.v(TAG, "Unknown PermissionChecker result: " + result); - return false; + INotificationManager service = getService(); + try { + return service.canUseFullScreenIntent(mContext.getAttributionSource()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 9bf56b374576..99a7fa21b911 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -841,7 +841,8 @@ public final class PendingIntent implements Parcelable { /** * Perform the operation associated with this PendingIntent. * - * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler) + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String, + * Bundle) * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. @@ -855,7 +856,8 @@ public final class PendingIntent implements Parcelable { * * @param code Result code to supply back to the PendingIntent's target. * - * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler) + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String, + * Bundle) * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. @@ -875,7 +877,8 @@ public final class PendingIntent implements Parcelable { * original Intent. If flag {@link #FLAG_IMMUTABLE} was set when this * pending intent was created, this argument will be ignored. * - * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler) + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String, + * Bundle) * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. @@ -892,6 +895,11 @@ public final class PendingIntent implements Parcelable { * @param options Additional options the caller would like to provide to modify the * sending behavior. May be built from an {@link ActivityOptions} to apply to an * activity start. + * + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String) + * + * @throws CanceledException Throws CanceledException if the PendingIntent + * is no longer allowing more intents to be sent through it. */ public void send(@Nullable Bundle options) throws CanceledException { send(null, 0, null, null, null, null, options); @@ -908,7 +916,8 @@ public final class PendingIntent implements Parcelable { * should happen. If null, the callback will happen from the thread * pool of the process. * - * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler) + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String, + * Bundle) * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. @@ -942,11 +951,8 @@ public final class PendingIntent implements Parcelable { * should happen. If null, the callback will happen from the thread * pool of the process. * - * @see #send() - * @see #send(int) - * @see #send(Context, int, Intent) - * @see #send(int, android.app.PendingIntent.OnFinished, Handler) - * @see #send(Context, int, Intent, OnFinished, Handler, String) + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String, + * Bundle) * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. @@ -985,11 +991,8 @@ public final class PendingIntent implements Parcelable { * {@link Context#sendBroadcast(Intent, String) Context.sendOrderedBroadcast(Intent, String)}. * If null, no permission is required. * - * @see #send() - * @see #send(int) - * @see #send(Context, int, Intent) - * @see #send(int, android.app.PendingIntent.OnFinished, Handler) - * @see #send(Context, int, Intent, OnFinished, Handler) + * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler, String, + * Bundle) * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. @@ -1032,12 +1035,6 @@ public final class PendingIntent implements Parcelable { * @param options Additional options the caller would like to provide to modify the sending * behavior. May be built from an {@link ActivityOptions} to apply to an activity start. * - * @see #send() - * @see #send(int) - * @see #send(Context, int, Intent) - * @see #send(int, android.app.PendingIntent.OnFinished, Handler) - * @see #send(Context, int, Intent, OnFinished, Handler) - * * @throws CanceledException Throws CanceledException if the PendingIntent * is no longer allowing more intents to be sent through it. */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c473d3f81823..7cb959dee245 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3431,7 +3431,7 @@ public final class Settings { + " type:" + mUri.getPath() + " in package:" + cr.getPackageName()); } - for (int i = 0; i < mValues.size(); ++i) { + for (int i = mValues.size() - 1; i >= 0; i--) { String key = mValues.keyAt(i); if (key.startsWith(prefix)) { mValues.remove(key); @@ -18125,12 +18125,6 @@ public final class Settings { public static final String WEAR_OS_VERSION_STRING = "wear_os_version_string"; /** - * Whether the physical button has been set. - * @hide - */ - public static final String BUTTON_SET = "button_set"; - - /** * Whether there is a side button. * @hide */ @@ -18302,6 +18296,12 @@ public final class Settings { public static final int COMPANION_OS_VERSION_UNDEFINED = -1; /** + * The companion App name. + * @hide + */ + public static final String COMPANION_APP_NAME = "wear_companion_app_name"; + + /** * A boolean value to indicate if we want to support all languages in LE edition on * wear. 1 for supporting, 0 for not supporting. * @hide @@ -18413,10 +18413,13 @@ public final class Settings { public static final String BURN_IN_PROTECTION_ENABLED = "burn_in_protection"; /** - * Whether the device has combined location setting enabled. + * + * @deprecated Use LocationManager as the source of truth for all location states. + * * @hide */ + @Deprecated public static final String COMBINED_LOCATION_ENABLED = "combined_location_enable"; /** @@ -18482,67 +18485,36 @@ public final class Settings { public static final String CLOCKWORK_LONG_PRESS_TO_ASSISTANT_ENABLED = "clockwork_long_press_to_assistant_enabled"; - /* + /** * Whether the device has Cooldown Mode enabled. * @hide */ public static final String COOLDOWN_MODE_ON = "cooldown_mode_on"; - /* + /** * Whether the device has Wet Mode/ Touch Lock Mode enabled. * @hide */ public static final String WET_MODE_ON = "wet_mode_on"; - /* + /** * Whether the RSB wake feature is enabled. * @hide */ public static final String RSB_WAKE_ENABLED = "rsb_wake_enabled"; - /* + /** * Whether the screen-unlock (keyguard) sound is enabled. * @hide */ public static final String SCREEN_UNLOCK_SOUND_ENABLED = "screen_unlock_sound_enabled"; - /* + /** * Whether charging sounds are enabled. * @hide */ public static final String CHARGING_SOUNDS_ENABLED = "wear_charging_sounds_enabled"; - /** The status of the early updates process. - * @hide - */ - public static final String EARLY_UPDATES_STATUS = "early_updates_status"; - - /** - * Early updates not started - * @hide - */ - public static final int EARLY_UPDATES_STATUS_NOT_STARTED = 0; - /** - * Early updates started and in progress - * @hide - */ - public static final int EARLY_UPDATES_STATUS_STARTED = 1; - /** - * Early updates completed and was successful - * @hide - */ - public static final int EARLY_UPDATES_STATUS_SUCCESS = 2; - /** - * Early updates skipped - * @hide - */ - public static final int EARLY_UPDATES_STATUS_SKIPPED = 3; - /** - * Early updates aborted due to timeout - * @hide - */ - public static final int EARLY_UPDATES_STATUS_ABORTED = 4; - /** * Whether dynamic color theming (e.g. Material You) is enabled for apps which support * it. @@ -18669,6 +18641,174 @@ public final class Settings { * @hide */ public static final int UPGRADE_DATA_MIGRATION_DONE = 2; + + /** + * Whether to disable AOD while plugged. + * (0 = false, 1 = true) + * @hide + */ + public static final String DISABLE_AOD_WHILE_PLUGGED = "disable_aod_while_plugged"; + + /** + * Whether the user has consented for network location provider (NLP). + * This setting key will only be used once during OOBE to set NLP initial value through + * the companion app ToS. This setting key will be synced over from Companion and + * corresponding toggle in GMS will be enabled. + * @hide + */ + public static final String NETWORK_LOCATION_OPT_IN = "network_location_opt_in"; + + /** + * The custom foreground color. + * @hide + */ + public static final String CUSTOM_COLOR_FOREGROUND = "custom_foreground_color"; + + /** + * The custom background color. + * @hide + */ + public static final String CUSTOM_COLOR_BACKGROUND = "custom_background_color"; + + /** The status of the phone switching process. + * @hide + */ + public static final String PHONE_SWITCHING_STATUS = "phone_switching_status"; + + /** + * Phone switching not started + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_NOT_STARTED = 0; + + /** + * Phone switching started + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_STARTED = 1; + + /** + * Phone switching completed and was successful + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_SUCCESS = 2; + + /** + * Phone switching was cancelled + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_CANCELLED = 3; + + /** + * Phone switching failed + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_FAILED = 4; + + /** + * Phone switching is in progress of advertising to new companion device. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_ADVERTISING = 5; + + /** + * Phone switching successfully bonded with new companion device. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_BONDED = 6; + + /** + * Phone switching successfully completed on phone side. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_PHONE_COMPLETE = 7; + + /** + * Connection config migration in progress. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION = 8; + + /** + * Connection config migration failed. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_FAILED = 9; + + /** + * Connection config migration cancellation in progress. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_CANCELLED = 10; + + /** + * Connection config migration success. + * @hide + */ + public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_SUCCESS = 11; + + + /** + * Whether the device has enabled the feature to reduce motion and animation + * (0 = false, 1 = true) + * @hide + */ + public static final String REDUCE_MOTION = "reduce_motion"; + + /** + * Whether RTL swipe-to-dismiss is enabled by developer options. + * (0 = false, 1 = true) + * @hide + */ + public static final String RTL_SWIPE_TO_DISMISS_ENABLED_DEV = + "rtl_swipe_to_dismiss_enabled_dev"; + + /** + * Tethered Configuration state. + * @hide + */ + public static final String TETHER_CONFIG_STATE = "tethered_config_state"; + + /** + * Tethered configuration state is unknown. + * @hide + */ + public static final int TETHERED_CONFIG_UNKNOWN = 0; + + /** + * Device is set into standalone mode. + * @hide + */ + public static final int TETHERED_CONFIG_STANDALONE = 1; + + /** + * Device is set in tethered mode. + * @hide + */ + public static final int TETHERED_CONFIG_TETHERED = 2; + + + /** + * Whether phone switching is supported. + * + * (0 = false, 1 = true) + * @hide + */ + public static final String PHONE_SWITCHING_SUPPORTED = "phone_switching_supported"; + + /** + * Setting indicating the name of the Wear OS package that hosts the Media Controls UI. + * + * @hide + */ + public static final String WEAR_MEDIA_CONTROLS_PACKAGE = "wear_media_controls_package"; + + /** + * Setting indicating the name of the Wear OS package responsible for bridging media. + * + * @hide + */ + public static final String WEAR_MEDIA_SESSIONS_PACKAGE = "wear_media_sessions_package"; } } diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index c92b1b8c120d..8c4e90c81147 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -195,7 +195,7 @@ public final class Choreographer { private boolean mDebugPrintNextFrameTimeDelta; private int mFPSDivisor = 1; - private final DisplayEventReceiver.VsyncEventData mLastVsyncEventData = + private DisplayEventReceiver.VsyncEventData mLastVsyncEventData = new DisplayEventReceiver.VsyncEventData(); private final FrameData mFrameData = new FrameData(); @@ -857,7 +857,7 @@ public final class Choreographer { mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; mLastFrameIntervalNanos = frameIntervalNanos; - mLastVsyncEventData.copyFrom(vsyncEventData); + mLastVsyncEventData = vsyncEventData; } AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS); @@ -1247,7 +1247,7 @@ public final class Choreographer { private boolean mHavePendingVsync; private long mTimestampNanos; private int mFrame; - private final VsyncEventData mLastVsyncEventData = new VsyncEventData(); + private VsyncEventData mLastVsyncEventData = new VsyncEventData(); FrameDisplayEventReceiver(Looper looper, int vsyncSource, long layerHandle) { super(looper, vsyncSource, /* eventRegistration */ 0, layerHandle); @@ -1287,7 +1287,7 @@ public final class Choreographer { mTimestampNanos = timestampNanos; mFrame = frame; - mLastVsyncEventData.copyFrom(vsyncEventData); + mLastVsyncEventData = vsyncEventData; Message msg = Message.obtain(mHandler, this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java index 54db34e788e9..03074894b2ff 100644 --- a/core/java/android/view/DisplayEventReceiver.java +++ b/core/java/android/view/DisplayEventReceiver.java @@ -81,10 +81,7 @@ public abstract class DisplayEventReceiver { // GC'd while the native peer of the receiver is using them. private MessageQueue mMessageQueue; - private final VsyncEventData mVsyncEventData = new VsyncEventData(); - private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver, - WeakReference<VsyncEventData> vsyncEventData, MessageQueue messageQueue, int vsyncSource, int eventRegistration, long layerHandle); private static native long nativeGetDisplayEventReceiverFinalizer(); @FastNative @@ -127,9 +124,7 @@ public abstract class DisplayEventReceiver { } mMessageQueue = looper.getQueue(); - mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), - new WeakReference<VsyncEventData>(mVsyncEventData), - mMessageQueue, + mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue, vsyncSource, eventRegistration, layerHandle); mFreeNativeResources = sNativeAllocationRegistry.registerNativeAllocation(this, mReceiverPtr); @@ -152,6 +147,9 @@ public abstract class DisplayEventReceiver { * @hide */ public static final class VsyncEventData { + static final FrameTimeline[] INVALID_FRAME_TIMELINES = + {new FrameTimeline(FrameInfo.INVALID_VSYNC_ID, Long.MAX_VALUE, Long.MAX_VALUE)}; + // The amount of frame timeline choices. // Must be in sync with VsyncEventData::kFrameTimelinesLength in // frameworks/native/libs/gui/include/gui/VsyncEventData.h. If they do not match, a runtime @@ -159,32 +157,22 @@ public abstract class DisplayEventReceiver { static final int FRAME_TIMELINES_LENGTH = 7; public static class FrameTimeline { - FrameTimeline() {} - - // Called from native code. - @SuppressWarnings("unused") FrameTimeline(long vsyncId, long expectedPresentationTime, long deadline) { this.vsyncId = vsyncId; this.expectedPresentationTime = expectedPresentationTime; this.deadline = deadline; } - void copyFrom(FrameTimeline other) { - vsyncId = other.vsyncId; - expectedPresentationTime = other.expectedPresentationTime; - deadline = other.deadline; - } - // The frame timeline vsync id, used to correlate a frame // produced by HWUI with the timeline data stored in Surface Flinger. - public long vsyncId = FrameInfo.INVALID_VSYNC_ID; + public final long vsyncId; // The frame timestamp for when the frame is expected to be presented. - public long expectedPresentationTime = Long.MAX_VALUE; + public final long expectedPresentationTime; // The frame deadline timestamp in {@link System#nanoTime()} timebase that it is // allotted for the frame to be completed. - public long deadline = Long.MAX_VALUE; + public final long deadline; } /** @@ -192,18 +180,11 @@ public abstract class DisplayEventReceiver { * {@link FrameInfo#VSYNC} to the current vsync in case Choreographer callback was heavily * delayed by the app. */ - public long frameInterval = -1; + public final long frameInterval; public final FrameTimeline[] frameTimelines; - public int preferredFrameTimelineIndex = 0; - - VsyncEventData() { - frameTimelines = new FrameTimeline[FRAME_TIMELINES_LENGTH]; - for (int i = 0; i < frameTimelines.length; i++) { - frameTimelines[i] = new FrameTimeline(); - } - } + public final int preferredFrameTimelineIndex; // Called from native code. @SuppressWarnings("unused") @@ -214,12 +195,10 @@ public abstract class DisplayEventReceiver { this.frameInterval = frameInterval; } - void copyFrom(VsyncEventData other) { - preferredFrameTimelineIndex = other.preferredFrameTimelineIndex; - frameInterval = other.frameInterval; - for (int i = 0; i < frameTimelines.length; i++) { - frameTimelines[i].copyFrom(other.frameTimelines[i]); - } + VsyncEventData() { + this.frameInterval = -1; + this.frameTimelines = INVALID_FRAME_TIMELINES; + this.preferredFrameTimelineIndex = 0; } public FrameTimeline preferredFrameTimeline() { @@ -325,8 +304,9 @@ public abstract class DisplayEventReceiver { // Called from native code. @SuppressWarnings("unused") - private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) { - onVsync(timestampNanos, physicalDisplayId, frame, mVsyncEventData); + private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame, + VsyncEventData vsyncEventData) { + onVsync(timestampNanos, physicalDisplayId, frame, vsyncEventData); } // Called from native code. diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java index d35aff9a72b7..3812d37a5fed 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -215,6 +215,7 @@ public final class InputWindowHandle { .append(", scaleFactor=").append(scaleFactor) .append(", transform=").append(transform) .append(", windowToken=").append(windowToken) + .append(", displayId=").append(displayId) .append(", isClone=").append((inputConfig & InputConfig.CLONE) != 0) .toString(); diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index ac50d09cc091..cd89a561074c 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -99,9 +99,16 @@ public class SurfaceControlViewHost { @Override public ISurfaceSyncGroup getSurfaceSyncGroup() { CompletableFuture<ISurfaceSyncGroup> surfaceSyncGroup = new CompletableFuture<>(); - mViewRoot.mHandler.post( - () -> surfaceSyncGroup.complete( - mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup)); + // If the call came from in process and it's already running on the UI thread, return + // results immediately instead of posting to the main thread. If we post to the main + // thread, it will block itself and the return value will always be null. + if (Thread.currentThread() == mViewRoot.mThread) { + return mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup; + } else { + mViewRoot.mHandler.post( + () -> surfaceSyncGroup.complete( + mViewRoot.getOrCreateSurfaceSyncGroup().mISurfaceSyncGroup)); + } try { return surfaceSyncGroup.get(1, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 18874f768929..3165654d806d 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1780,6 +1780,21 @@ public class RemoteViews implements Parcelable, Filter { Object value = getParameterValue(view); try { MethodHandle method = getMethod(view, this.methodName, param, true /* async */); + // Upload the bitmap to GPU if the parameter is of type Bitmap or Icon. + // Since bitmaps in framework are seldomly modified, this is supposed to accelerate + // the operations. + if (value instanceof Bitmap bitmap) { + bitmap.prepareToDraw(); + } + + if (value instanceof Icon icon + && (icon.getType() == Icon.TYPE_BITMAP + || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) { + Bitmap bitmap = icon.getBitmap(); + if (bitmap != null) { + bitmap.prepareToDraw(); + } + } if (method != null) { Runnable endAction = (Runnable) method.invoke(view, value); diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index 410b44161cf6..dd72689206ba 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -48,22 +48,12 @@ static struct { struct { jclass clazz; - jmethodID init; - - jfieldID vsyncId; - jfieldID expectedPresentationTime; - jfieldID deadline; } frameTimelineClassInfo; struct { jclass clazz; - jmethodID init; - - jfieldID frameInterval; - jfieldID preferredFrameTimelineIndex; - jfieldID frameTimelines; } vsyncEventDataClassInfo; } gDisplayEventReceiverClassInfo; @@ -71,7 +61,7 @@ static struct { class NativeDisplayEventReceiver : public DisplayEventDispatcher { public: - NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, jobject vsyncEventDataWeak, + NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, const sp<MessageQueue>& messageQueue, jint vsyncSource, jint eventRegistration, jlong layerHandle); @@ -82,7 +72,6 @@ protected: private: jobject mReceiverWeakGlobal; - jobject mVsyncEventDataWeakGlobal; sp<MessageQueue> mMessageQueue; void dispatchVsync(nsecs_t timestamp, PhysicalDisplayId displayId, uint32_t count, @@ -96,7 +85,6 @@ private: }; NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak, - jobject vsyncEventDataWeak, const sp<MessageQueue>& messageQueue, jint vsyncSource, jint eventRegistration, jlong layerHandle) @@ -108,7 +96,6 @@ NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject rece reinterpret_cast<IBinder*>(layerHandle)) : nullptr), mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)), - mVsyncEventDataWeakGlobal(env->NewGlobalRef(vsyncEventDataWeak)), mMessageQueue(messageQueue) { ALOGV("receiver %p ~ Initializing display event receiver.", this); } @@ -167,43 +154,12 @@ void NativeDisplayEventReceiver::dispatchVsync(nsecs_t timestamp, PhysicalDispla JNIEnv* env = AndroidRuntime::getJNIEnv(); ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal)); - ScopedLocalRef<jobject> vsyncEventDataObj(env, GetReferent(env, mVsyncEventDataWeakGlobal)); - if (receiverObj.get() && vsyncEventDataObj.get()) { + if (receiverObj.get()) { ALOGV("receiver %p ~ Invoking vsync handler.", this); - env->SetIntField(vsyncEventDataObj.get(), - gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo - .preferredFrameTimelineIndex, - vsyncEventData.preferredFrameTimelineIndex); - env->SetLongField(vsyncEventDataObj.get(), - gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.frameInterval, - vsyncEventData.frameInterval); - - ScopedLocalRef<jobjectArray> - frameTimelinesObj(env, - reinterpret_cast<jobjectArray>( - env->GetObjectField(vsyncEventDataObj.get(), - gDisplayEventReceiverClassInfo - .vsyncEventDataClassInfo - .frameTimelines))); - for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) { - VsyncEventData::FrameTimeline& frameTimeline = vsyncEventData.frameTimelines[i]; - ScopedLocalRef<jobject> - frameTimelineObj(env, env->GetObjectArrayElement(frameTimelinesObj.get(), i)); - env->SetLongField(frameTimelineObj.get(), - gDisplayEventReceiverClassInfo.frameTimelineClassInfo.vsyncId, - frameTimeline.vsyncId); - env->SetLongField(frameTimelineObj.get(), - gDisplayEventReceiverClassInfo.frameTimelineClassInfo - .expectedPresentationTime, - frameTimeline.expectedPresentationTime); - env->SetLongField(frameTimelineObj.get(), - gDisplayEventReceiverClassInfo.frameTimelineClassInfo.deadline, - frameTimeline.deadlineTimestamp); - } - + jobject javaVsyncEventData = createJavaVsyncEventData(env, vsyncEventData); env->CallVoidMethod(receiverObj.get(), gDisplayEventReceiverClassInfo.dispatchVsync, - timestamp, displayId.value, count); + timestamp, displayId.value, count, javaVsyncEventData); ALOGV("receiver %p ~ Returned from vsync handler.", this); } @@ -271,9 +227,8 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides( mMessageQueue->raiseAndClearException(env, "dispatchModeChanged"); } -static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject vsyncEventDataWeak, - jobject messageQueueObj, jint vsyncSource, jint eventRegistration, - jlong layerHandle) { +static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj, + jint vsyncSource, jint eventRegistration, jlong layerHandle) { sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj); if (messageQueue == NULL) { jniThrowRuntimeException(env, "MessageQueue is not initialized."); @@ -281,8 +236,8 @@ static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject } sp<NativeDisplayEventReceiver> receiver = - new NativeDisplayEventReceiver(env, receiverWeak, vsyncEventDataWeak, messageQueue, - vsyncSource, eventRegistration, layerHandle); + new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource, + eventRegistration, layerHandle); status_t status = receiver->initialize(); if (status) { String8 message; @@ -329,9 +284,7 @@ static jobject nativeGetLatestVsyncEventData(JNIEnv* env, jclass clazz, jlong re static const JNINativeMethod gMethods[] = { /* name, signature, funcPtr */ - {"nativeInit", - "(Ljava/lang/ref/WeakReference;Ljava/lang/ref/WeakReference;Landroid/os/" - "MessageQueue;IIJ)J", + {"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;IIJ)J", (void*)nativeInit}, {"nativeGetDisplayEventReceiverFinalizer", "()J", (void*)nativeGetDisplayEventReceiverFinalizer}, @@ -348,7 +301,8 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { gDisplayEventReceiverClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); gDisplayEventReceiverClassInfo.dispatchVsync = - GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchVsync", "(JJI)V"); + GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchVsync", + "(JJILandroid/view/DisplayEventReceiver$VsyncEventData;)V"); gDisplayEventReceiverClassInfo.dispatchHotplug = GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug", "(JJZ)V"); gDisplayEventReceiverClassInfo.dispatchModeChanged = @@ -374,15 +328,6 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { gDisplayEventReceiverClassInfo.frameTimelineClassInfo.init = GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, "<init>", "(JJJ)V"); - gDisplayEventReceiverClassInfo.frameTimelineClassInfo.vsyncId = - GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, - "vsyncId", "J"); - gDisplayEventReceiverClassInfo.frameTimelineClassInfo.expectedPresentationTime = - GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, - "expectedPresentationTime", "J"); - gDisplayEventReceiverClassInfo.frameTimelineClassInfo.deadline = - GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.frameTimelineClassInfo.clazz, - "deadline", "J"); jclass vsyncEventDataClazz = FindClassOrDie(env, "android/view/DisplayEventReceiver$VsyncEventData"); @@ -394,17 +339,6 @@ int register_android_view_DisplayEventReceiver(JNIEnv* env) { "([Landroid/view/" "DisplayEventReceiver$VsyncEventData$FrameTimeline;IJ)V"); - gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.preferredFrameTimelineIndex = - GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, - "preferredFrameTimelineIndex", "I"); - gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.frameInterval = - GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, - "frameInterval", "J"); - gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.frameTimelines = - GetFieldIDOrDie(env, gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, - "frameTimelines", - "[Landroid/view/DisplayEventReceiver$VsyncEventData$FrameTimeline;"); - return res; } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 3ff63519572f..e4d74b5373e0 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6067,6 +6067,12 @@ <!-- Wear OS: the name of the main activity of the device's sysui. --> <string name="config_wearSysUiMainActivity" translatable="false"/> + <!-- Wear OS: the name of the package containing the Media Controls Activity. --> + <string name="config_wearMediaControlsPackage" translatable="false"/> + + <!-- Wear OS: the name of the package containing the Media Sessions APK. --> + <string name="config_wearMediaSessionsPackage" translatable="false"/> + <bool name="config_secondaryBuiltInDisplayIsRound">@bool/config_windowIsRound</bool> <!-- The display round config for each display in a multi-display device. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 7b582da836aa..c6c1c8f120d7 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3138,7 +3138,6 @@ <!-- Work profile unlaunchable app alert dialog--> <java-symbol type="style" name="AlertDialogWithEmergencyButton"/> - <java-symbol type="string" name="work_mode_dialer_off_message" /> <java-symbol type="string" name="work_mode_emergency_call_button" /> <java-symbol type="string" name="work_mode_off_title" /> <java-symbol type="string" name="work_mode_off_message" /> @@ -4865,6 +4864,8 @@ <java-symbol type="string" name="config_wearSysUiPackage"/> <java-symbol type="string" name="config_wearSysUiMainActivity"/> + <java-symbol type="string" name="config_wearMediaControlsPackage"/> + <java-symbol type="string" name="config_wearMediaSessionsPackage"/> <java-symbol type="string" name="config_defaultQrCodeComponent"/> <java-symbol type="dimen" name="secondary_rounded_corner_radius" /> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index c106854c700b..0eb4caaf7a0f 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -475,6 +475,18 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RecentTasks.java" }, + "-1643780158": { + "message": "Saving original orientation before camera compat, last orientation is %d", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, + "-1639406696": { + "message": "NOSENSOR override detected", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationReversionController.java" + }, "-1638958146": { "message": "Removing activity %s from task=%s adding to task=%s Callers=%s", "level": "INFO", @@ -751,6 +763,12 @@ "group": "WM_DEBUG_IME", "at": "com\/android\/server\/wm\/ImeInsetsSourceProvider.java" }, + "-1397175017": { + "message": "Other orientation overrides are in place: not reverting", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationReversionController.java" + }, "-1394745488": { "message": "ControlAdapter onAnimationCancelled mSource: %s mControlTarget: %s", "level": "INFO", @@ -1303,6 +1321,12 @@ "group": "WM_DEBUG_BOOT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-874087484": { + "message": "SyncGroup %d: Set ready %b", + "level": "VERBOSE", + "group": "WM_DEBUG_SYNC_ENGINE", + "at": "com\/android\/server\/wm\/BLASTSyncEngine.java" + }, "-869242375": { "message": "Content Recording: Unable to start recording due to invalid region for display %d", "level": "VERBOSE", @@ -1705,6 +1729,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, + "-529187878": { + "message": "Reverting orientation after camera compat force rotation", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-521613870": { "message": "App died during pause, not stopping: %s", "level": "VERBOSE", @@ -2377,6 +2407,12 @@ "group": "WM_DEBUG_FOCUS_LIGHT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "138097009": { + "message": "NOSENSOR override is absent: reverting", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationReversionController.java" + }, "140319294": { "message": "IME target changed within ActivityRecord", "level": "DEBUG", @@ -4009,12 +4045,6 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1689989893": { - "message": "SyncGroup %d: Set ready", - "level": "VERBOSE", - "group": "WM_DEBUG_SYNC_ENGINE", - "at": "com\/android\/server\/wm\/BLASTSyncEngine.java" - }, "1699269281": { "message": "Don't organize or trigger events for untrusted displayId=%d", "level": "WARN", diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 8dd23b70ae61..25b074d20b81 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -1921,6 +1921,7 @@ public final class Bitmap implements Parcelable { */ public void setGainmap(@Nullable Gainmap gainmap) { checkRecycled("Bitmap is recycled"); + mGainmap = null; nativeSetGainmap(mNativePtr, gainmap == null ? 0 : gainmap.mNativePtr); } diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java index 701e20c499da..1da8e189d768 100644 --- a/graphics/java/android/graphics/BitmapFactory.java +++ b/graphics/java/android/graphics/BitmapFactory.java @@ -482,7 +482,9 @@ public class BitmapFactory { if (opts == null || opts.inBitmap == null) { return 0; } - + // Clear out the gainmap since we don't attempt to reuse it and don't want to + // accidentally keep it on the re-used bitmap + opts.inBitmap.setGainmap(null); return opts.inBitmap.getNativeInstance(); } diff --git a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java index d785c3c895b8..f26b50ed4e2a 100644 --- a/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java +++ b/identity/java/android/security/identity/CredstoreIdentityCredentialStore.java @@ -21,10 +21,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; -import android.os.RemoteException; import android.os.ServiceManager; -import android.security.GenerateRkpKey; -import android.security.keymaster.KeymasterDefs; class CredstoreIdentityCredentialStore extends IdentityCredentialStore { @@ -125,18 +122,7 @@ class CredstoreIdentityCredentialStore extends IdentityCredentialStore { @NonNull String docType) throws AlreadyPersonalizedException, DocTypeNotSupportedException { try { - IWritableCredential wc; - wc = mStore.createCredential(credentialName, docType); - try { - GenerateRkpKey keyGen = new GenerateRkpKey(mContext); - // We don't know what the security level is for the backing keymint, so go ahead and - // poke the provisioner for both TEE and SB. - keyGen.notifyKeyGenerated(KeymasterDefs.KM_SECURITY_LEVEL_TRUSTED_ENVIRONMENT); - keyGen.notifyKeyGenerated(KeymasterDefs.KM_SECURITY_LEVEL_STRONGBOX); - } catch (RemoteException e) { - // Not really an error state. Does not apply at all if RKP is unsupported or - // disabled on a given device. - } + IWritableCredential wc = mStore.createCredential(credentialName, docType); return new CredstoreWritableIdentityCredential(mContext, credentialName, docType, wc); } catch (android.os.RemoteException e) { throw new RuntimeException("Unexpected RemoteException ", e); diff --git a/keystore/java/android/security/GenerateRkpKey.java b/keystore/java/android/security/GenerateRkpKey.java deleted file mode 100644 index 698133287f63..000000000000 --- a/keystore/java/android/security/GenerateRkpKey.java +++ /dev/null @@ -1,159 +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 android.security; - -import android.annotation.CheckResult; -import android.annotation.IntDef; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -/** - * GenerateKey is a helper class to handle interactions between Keystore and the RemoteProvisioner - * app. There are two cases where Keystore should use this class. - * - * (1) : An app generates a new attested key pair, so Keystore calls notifyKeyGenerated to let the - * RemoteProvisioner app check if the state of the attestation key pool is getting low enough - * to warrant provisioning more attestation certificates early. - * - * (2) : An app attempts to generate a new key pair, but the keystore service discovers it is out of - * attestation key pairs and cannot provide one for the given application. Keystore can then - * make a blocking call on notifyEmpty to allow the RemoteProvisioner app to get another - * attestation certificate chain provisioned. - * - * In most cases, the proper usage of (1) should preclude the need for (2). - * - * @hide - */ -public class GenerateRkpKey { - private static final String TAG = "GenerateRkpKey"; - - private static final int NOTIFY_EMPTY = 0; - private static final int NOTIFY_KEY_GENERATED = 1; - private static final int TIMEOUT_MS = 1000; - - private IGenerateRkpKeyService mBinder; - private Context mContext; - private CountDownLatch mCountDownLatch; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = { - IGenerateRkpKeyService.Status.OK, - IGenerateRkpKeyService.Status.NO_NETWORK_CONNECTIVITY, - IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR, - IGenerateRkpKeyService.Status.DEVICE_NOT_REGISTERED, - IGenerateRkpKeyService.Status.HTTP_CLIENT_ERROR, - IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR, - IGenerateRkpKeyService.Status.HTTP_UNKNOWN_ERROR, - IGenerateRkpKeyService.Status.INTERNAL_ERROR, - }) - public @interface Status { - } - - private ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - mBinder = IGenerateRkpKeyService.Stub.asInterface(service); - mCountDownLatch.countDown(); - } - - @Override public void onBindingDied(ComponentName className) { - mCountDownLatch.countDown(); - } - - @Override - public void onServiceDisconnected(ComponentName className) { - mBinder = null; - } - }; - - /** - * Constructor which takes a Context object. - */ - public GenerateRkpKey(Context context) { - mContext = context; - } - - @Status - private int bindAndSendCommand(int command, int securityLevel) throws RemoteException { - Intent intent = new Intent(IGenerateRkpKeyService.class.getName()); - ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); - int returnCode = IGenerateRkpKeyService.Status.OK; - if (comp == null) { - // On a system that does not use RKP, the RemoteProvisioner app won't be installed. - return returnCode; - } - intent.setComponent(comp); - mCountDownLatch = new CountDownLatch(1); - Executor executor = Executors.newCachedThreadPool(); - if (!mContext.bindService(intent, Context.BIND_AUTO_CREATE, executor, mConnection)) { - throw new RemoteException("Failed to bind to GenerateRkpKeyService"); - } - try { - mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Log.e(TAG, "Interrupted: ", e); - } - if (mBinder != null) { - switch (command) { - case NOTIFY_EMPTY: - returnCode = mBinder.generateKey(securityLevel); - break; - case NOTIFY_KEY_GENERATED: - mBinder.notifyKeyGenerated(securityLevel); - break; - default: - Log.e(TAG, "Invalid case for command"); - } - } else { - Log.e(TAG, "Binder object is null; failed to bind to GenerateRkpKeyService."); - returnCode = IGenerateRkpKeyService.Status.INTERNAL_ERROR; - } - mContext.unbindService(mConnection); - return returnCode; - } - - /** - * Fulfills the use case of (2) described in the class documentation. Blocks until the - * RemoteProvisioner application can get new attestation keys signed by the server. - * @return the status of the key generation - */ - @CheckResult - @Status - public int notifyEmpty(int securityLevel) throws RemoteException { - return bindAndSendCommand(NOTIFY_EMPTY, securityLevel); - } - - /** - * Fulfills the use case of (1) described in the class documentation. Non blocking call. - */ - public void notifyKeyGenerated(int securityLevel) throws RemoteException { - bindAndSendCommand(NOTIFY_KEY_GENERATED, securityLevel); - } -} diff --git a/keystore/java/android/security/IGenerateRkpKeyService.aidl b/keystore/java/android/security/IGenerateRkpKeyService.aidl deleted file mode 100644 index eeaeb27a7c77..000000000000 --- a/keystore/java/android/security/IGenerateRkpKeyService.aidl +++ /dev/null @@ -1,60 +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 android.security; - -/** - * Interface to allow the framework to notify the RemoteProvisioner app when keys are empty. This - * will be used if Keystore replies with an error code NO_KEYS_AVAILABLE in response to an - * attestation request. The framework can then synchronously call generateKey() to get more - * attestation keys generated and signed. Upon return, the caller can be certain an attestation key - * is available. - * - * @hide - */ -interface IGenerateRkpKeyService { - @JavaDerive(toString=true) - @Backing(type="int") - enum Status { - /** No error(s) occurred */ - OK = 0, - /** Unable to provision keys due to a lack of internet connectivity. */ - NO_NETWORK_CONNECTIVITY = 1, - /** An error occurred while communicating with the RKP server. */ - NETWORK_COMMUNICATION_ERROR = 2, - /** The given device was not registered with the RKP backend. */ - DEVICE_NOT_REGISTERED = 4, - /** The RKP server returned an HTTP client error, indicating a misbehaving client. */ - HTTP_CLIENT_ERROR = 5, - /** The RKP server returned an HTTP server error, indicating something went wrong on the server. */ - HTTP_SERVER_ERROR = 6, - /** The RKP server returned an HTTP status that is unknown. This should never happen. */ - HTTP_UNKNOWN_ERROR = 7, - /** An unexpected internal error occurred. This should never happen. */ - INTERNAL_ERROR = 8, - } - - /** - * Ping the provisioner service to let it know an app generated a key. This may or may not have - * consumed a remotely provisioned attestation key, so the RemoteProvisioner app should check. - */ - oneway void notifyKeyGenerated(in int securityLevel); - - /** - * Ping the provisioner service to indicate there are no remaining attestation keys left. - */ - Status generateKey(in int securityLevel); -} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java index c3b0f9bc16d3..474b7ea56be9 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -20,7 +20,6 @@ import static android.security.keystore2.AndroidKeyStoreCipherSpiBase.DEFAULT_MG import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityThread; import android.content.Context; import android.hardware.security.keymint.EcCurve; import android.hardware.security.keymint.KeyParameter; @@ -28,9 +27,6 @@ import android.hardware.security.keymint.KeyPurpose; import android.hardware.security.keymint.SecurityLevel; import android.hardware.security.keymint.Tag; import android.os.Build; -import android.os.RemoteException; -import android.security.GenerateRkpKey; -import android.security.IGenerateRkpKeyService; import android.security.KeyPairGeneratorSpec; import android.security.KeyStore2; import android.security.KeyStoreException; @@ -621,45 +617,6 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato @Override public KeyPair generateKeyPair() { - GenerateKeyPairHelperResult result = new GenerateKeyPairHelperResult(0, null); - for (int i = 0; i < 2; i++) { - /** - * NOTE: There is no need to delay between re-tries because the call to - * GenerateRkpKey.notifyEmpty() will delay for a while before returning. - */ - result = generateKeyPairHelper(); - if (result.rkpStatus == KeyStoreException.RKP_SUCCESS && result.keyPair != null) { - return result.keyPair; - } - } - - // RKP failure - if (result.rkpStatus != KeyStoreException.RKP_SUCCESS) { - KeyStoreException ksException = new KeyStoreException(ResponseCode.OUT_OF_KEYS, - "Could not get RKP keys", result.rkpStatus); - throw new ProviderException("Failed to provision new attestation keys.", ksException); - } - - return result.keyPair; - } - - private static class GenerateKeyPairHelperResult { - // Zero indicates success, non-zero indicates failure. Values should be - // {@link android.security.KeyStoreException#RKP_TEMPORARILY_UNAVAILABLE}, - // {@link android.security.KeyStoreException#RKP_SERVER_REFUSED_ISSUANCE}, - // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_CONNECTIVITY} - // {@link android.security.KeyStoreException#RKP_FETCHING_PENDING_SOFTWARE_REBOOT} - public final int rkpStatus; - @Nullable - public final KeyPair keyPair; - - private GenerateKeyPairHelperResult(int rkpStatus, KeyPair keyPair) { - this.rkpStatus = rkpStatus; - this.keyPair = keyPair; - } - } - - private GenerateKeyPairHelperResult generateKeyPairHelper() { if (mKeyStore == null || mSpec == null) { throw new IllegalStateException("Not initialized"); } @@ -697,26 +654,12 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato AndroidKeyStorePublicKey publicKey = AndroidKeyStoreProvider.makeAndroidKeyStorePublicKeyFromKeyEntryResponse( descriptor, metadata, iSecurityLevel, mKeymasterAlgorithm); - GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread - .currentApplication()); - try { - if (mSpec.getAttestationChallenge() != null) { - keyGen.notifyKeyGenerated(securityLevel); - } - } catch (RemoteException e) { - // This is not really an error state, and necessarily does not apply to non RKP - // systems or hybrid systems where RKP is not currently turned on. - Log.d(TAG, "Couldn't connect to the RemoteProvisioner backend.", e); - } success = true; - KeyPair kp = new KeyPair(publicKey, publicKey.getPrivateKey()); - return new GenerateKeyPairHelperResult(0, kp); + return new KeyPair(publicKey, publicKey.getPrivateKey()); } catch (KeyStoreException e) { switch (e.getErrorCode()) { case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE: throw new StrongBoxUnavailableException("Failed to generated key pair.", e); - case ResponseCode.OUT_OF_KEYS: - return checkIfRetryableOrThrow(e, securityLevel); default: ProviderException p = new ProviderException("Failed to generate key pair.", e); if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) { @@ -742,55 +685,6 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato } } - // In case keystore reports OUT_OF_KEYS, call this handler in an attempt to remotely provision - // some keys. - GenerateKeyPairHelperResult checkIfRetryableOrThrow(KeyStoreException e, int securityLevel) { - GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread - .currentApplication()); - KeyStoreException ksException; - try { - final int keyGenStatus = keyGen.notifyEmpty(securityLevel); - // Default stance: temporary error. This is a hint to the caller to try again with - // exponential back-off. - int rkpStatus; - switch (keyGenStatus) { - case IGenerateRkpKeyService.Status.NO_NETWORK_CONNECTIVITY: - rkpStatus = KeyStoreException.RKP_FETCHING_PENDING_CONNECTIVITY; - break; - case IGenerateRkpKeyService.Status.DEVICE_NOT_REGISTERED: - rkpStatus = KeyStoreException.RKP_SERVER_REFUSED_ISSUANCE; - break; - case IGenerateRkpKeyService.Status.OK: - // Explicitly return not-OK here so we retry in generateKeyPair. All other cases - // should throw because a retry doesn't make sense if we didn't actually - // provision fresh keys. - return new GenerateKeyPairHelperResult( - KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE, null); - case IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR: - case IGenerateRkpKeyService.Status.HTTP_CLIENT_ERROR: - case IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR: - case IGenerateRkpKeyService.Status.HTTP_UNKNOWN_ERROR: - case IGenerateRkpKeyService.Status.INTERNAL_ERROR: - default: - // These errors really should never happen. The best we can do is assume they - // are transient and hint to the caller to retry with back-off. - rkpStatus = KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE; - break; - } - ksException = new KeyStoreException( - ResponseCode.OUT_OF_KEYS, - "Out of RKP keys due to IGenerateRkpKeyService status: " + keyGenStatus, - rkpStatus); - } catch (RemoteException f) { - ksException = new KeyStoreException( - ResponseCode.OUT_OF_KEYS, - "Remote exception: " + f.getMessage(), - KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE); - } - ksException.initCause(e); - throw new ProviderException("Failed to provision new attestation keys.", ksException); - } - private void addAttestationParameters(@NonNull List<KeyParameter> params) throws ProviderException, IllegalArgumentException, DeviceIdAttestationException { byte[] challenge = mSpec.getAttestationChallenge(); 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 f99821747fef..afc573e4fcbb 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 @@ -197,7 +197,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { - if (mTransitionPausingRelayout.equals(merged)) { + if (merged.equals(mTransitionPausingRelayout)) { mTransitionPausingRelayout = playing; } } @@ -312,8 +312,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.back_button) { mTaskOperations.injectBackKey(); } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { - moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId)); - decoration.createHandleMenu(); + if (!decoration.isHandleMenuActive()) { + moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId)); + decoration.createHandleMenu(); + } else { + decoration.closeHandleMenu(); + } } else if (id == R.id.desktop_button) { mDesktopModeController.ifPresent(c -> c.setDesktopModeActive(true)); mDesktopTasksController.ifPresent(c -> c.moveToDesktop(mTaskId)); 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 efc90b5e63e1..f9c0e600dd38 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 @@ -487,6 +487,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mHandleMenuAppInfoPill.mWindowViewHost.getView().getWidth() == 0) return; PointF inputPoint = offsetCaptionLocation(ev); + + // If this is called before open_menu_button's onClick, we don't want to close + // the menu since it will just reopen in onClick. + final boolean pointInOpenMenuButton = pointInView( + mResult.mRootView.findViewById(R.id.open_menu_button), + inputPoint.x, + inputPoint.y); + final boolean pointInAppInfoPill = pointInView( mHandleMenuAppInfoPill.mWindowViewHost.getView(), inputPoint.x - mHandleMenuAppInfoPillPosition.x - mResult.mDecorContainerOffsetX, @@ -506,7 +514,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin - mResult.mDecorContainerOffsetX, inputPoint.y - mHandleMenuMoreActionsPillPosition.y - mResult.mDecorContainerOffsetY); - if (!pointInAppInfoPill && !pointInWindowingPill && !pointInMoreActionsPill) { + if (!pointInAppInfoPill && !pointInWindowingPill + && !pointInMoreActionsPill && !pointInOpenMenuButton) { closeHandleMenu(); } } diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index d08bc5c583c2..8049dc946c9e 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -29,9 +29,10 @@ namespace android { -AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed) - : mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed) { - mTimeToShowNextSnapshot = ms2ns(mSkAnimatedImage->currentFrameDuration()); +AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed, + SkEncodedImageFormat format) + : mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed), mFormat(format) { + mTimeToShowNextSnapshot = ms2ns(currentFrameDuration()); setStagingBounds(mSkAnimatedImage->getBounds()); } @@ -92,7 +93,7 @@ bool AnimatedImageDrawable::isDirty(nsecs_t* outDelay) { // directly from mSkAnimatedImage. lock.unlock(); std::unique_lock imageLock{mImageLock}; - *outDelay = ms2ns(mSkAnimatedImage->currentFrameDuration()); + *outDelay = ms2ns(currentFrameDuration()); return true; } else { // The next snapshot has not yet been decoded, but we've already passed @@ -109,7 +110,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::decodeNextFrame() { Snapshot snap; { std::unique_lock lock{mImageLock}; - snap.mDurationMS = mSkAnimatedImage->decodeNextFrame(); + snap.mDurationMS = adjustFrameDuration(mSkAnimatedImage->decodeNextFrame()); snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot()); } @@ -123,7 +124,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::reset() { std::unique_lock lock{mImageLock}; mSkAnimatedImage->reset(); snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot()); - snap.mDurationMS = mSkAnimatedImage->currentFrameDuration(); + snap.mDurationMS = currentFrameDuration(); } return snap; @@ -274,7 +275,7 @@ int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) { { std::unique_lock lock{mImageLock}; mSkAnimatedImage->reset(); - durationMS = mSkAnimatedImage->currentFrameDuration(); + durationMS = currentFrameDuration(); } { std::unique_lock lock{mSwapLock}; @@ -306,7 +307,7 @@ int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) { { std::unique_lock lock{mImageLock}; if (update) { - durationMS = mSkAnimatedImage->decodeNextFrame(); + durationMS = adjustFrameDuration(mSkAnimatedImage->decodeNextFrame()); } canvas->drawDrawable(mSkAnimatedImage.get()); @@ -336,4 +337,20 @@ SkRect AnimatedImageDrawable::onGetBounds() { return SkRectMakeLargest(); } +int AnimatedImageDrawable::adjustFrameDuration(int durationMs) { + if (durationMs == SkAnimatedImage::kFinished) { + return SkAnimatedImage::kFinished; + } + + if (mFormat == SkEncodedImageFormat::kGIF) { + // Match Chrome & Firefox behavior that gifs with a duration <= 10ms is bumped to 100ms + return durationMs <= 10 ? 100 : durationMs; + } + return durationMs; +} + +int AnimatedImageDrawable::currentFrameDuration() { + return adjustFrameDuration(mSkAnimatedImage->currentFrameDuration()); +} + } // namespace android diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h index 8ca3c7e125f1..1e965abc82b5 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.h +++ b/libs/hwui/hwui/AnimatedImageDrawable.h @@ -16,16 +16,16 @@ #pragma once -#include <cutils/compiler.h> -#include <utils/Macros.h> -#include <utils/RefBase.h> -#include <utils/Timers.h> - #include <SkAnimatedImage.h> #include <SkCanvas.h> #include <SkColorFilter.h> #include <SkDrawable.h> +#include <SkEncodedImageFormat.h> #include <SkPicture.h> +#include <cutils/compiler.h> +#include <utils/Macros.h> +#include <utils/RefBase.h> +#include <utils/Timers.h> #include <future> #include <mutex> @@ -48,7 +48,8 @@ class AnimatedImageDrawable : public SkDrawable { public: // bytesUsed includes the approximate sizes of the SkAnimatedImage and the SkPictures in the // Snapshots. - AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed); + AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed, + SkEncodedImageFormat format); /** * This updates the internal time and returns true if the image needs @@ -115,6 +116,7 @@ protected: private: sk_sp<SkAnimatedImage> mSkAnimatedImage; const size_t mBytesUsed; + const SkEncodedImageFormat mFormat; bool mRunning = false; bool mStarting = false; @@ -157,6 +159,9 @@ private: Properties mProperties; std::unique_ptr<OnAnimationEndListener> mEndListener; + + int adjustFrameDuration(int); + int currentFrameDuration(); }; } // namespace android diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index 373e893b9a25..a7f5aa83e624 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -97,7 +97,7 @@ static jlong AnimatedImageDrawable_nCreate(JNIEnv* env, jobject /*clazz*/, bytesUsed += picture->approximateBytesUsed(); } - + SkEncodedImageFormat format = imageDecoder->mCodec->getEncodedFormat(); sk_sp<SkAnimatedImage> animatedImg = SkAnimatedImage::Make(std::move(imageDecoder->mCodec), info, subset, std::move(picture)); @@ -108,8 +108,8 @@ static jlong AnimatedImageDrawable_nCreate(JNIEnv* env, jobject /*clazz*/, bytesUsed += sizeof(animatedImg.get()); - sk_sp<AnimatedImageDrawable> drawable(new AnimatedImageDrawable(std::move(animatedImg), - bytesUsed)); + sk_sp<AnimatedImageDrawable> drawable( + new AnimatedImageDrawable(std::move(animatedImg), bytesUsed, format)); return reinterpret_cast<jlong>(drawable.release()); } diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index cc987bcd8f0e..2a8cb42f7675 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -104,7 +104,8 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( GrBackendRenderTarget backendRT(frame.width(), frame.height(), 0, STENCIL_BUFFER_SIZE, fboInfo); - SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + SkSurfaceProps props(mColorMode == ColorMode::Default ? 0 : SkSurfaceProps::kAlwaysDither_Flag, + kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); sk_sp<SkSurface> surface; diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 1f929685b62c..b020e966e05a 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -28,7 +28,6 @@ #include <SkMultiPictureDocument.h> #include <SkOverdrawCanvas.h> #include <SkOverdrawColorFilter.h> -#include <SkPaintFilterCanvas.h> #include <SkPicture.h> #include <SkPictureRecorder.h> #include <SkRect.h> @@ -450,23 +449,6 @@ void SkiaPipeline::endCapture(SkSurface* surface) { } } -class ForceDitherCanvas : public SkPaintFilterCanvas { -public: - ForceDitherCanvas(SkCanvas* canvas) : SkPaintFilterCanvas(canvas) {} - -protected: - bool onFilter(SkPaint& paint) const override { - paint.setDither(true); - return true; - } - - void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override { - // We unroll the drawable using "this" canvas, so that draw calls contained inside will - // get dithering applied - drawable->draw(this, matrix); - } -}; - void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip, const std::vector<sp<RenderNode>>& nodes, bool opaque, const Rect& contentDrawBounds, sk_sp<SkSurface> surface, @@ -521,12 +503,6 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, canvas->clear(SK_ColorTRANSPARENT); } - std::optional<ForceDitherCanvas> forceDitherCanvas; - if (shouldForceDither()) { - forceDitherCanvas.emplace(canvas); - canvas = &forceDitherCanvas.value(); - } - if (1 == nodes.size()) { if (!nodes[0]->nothingToDraw()) { RenderNodeDrawable root(nodes[0].get(), canvas); diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index 0763b06b53ef..befee8989383 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -98,8 +98,6 @@ protected: bool isCapturingSkp() const { return mCaptureMode != CaptureMode::None; } - virtual bool shouldForceDither() const { return mColorMode != ColorMode::Default; } - private: void renderFrameImpl(const SkRect& clip, const std::vector<sp<RenderNode>>& nodes, bool opaque, diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index f22652f92c15..86096d5bd01c 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -203,11 +203,6 @@ sk_sp<Bitmap> SkiaVulkanPipeline::allocateHardwareBitmap(renderthread::RenderThr return nullptr; } -bool SkiaVulkanPipeline::shouldForceDither() const { - if (mVkSurface && mVkSurface->isBeyond8Bit()) return false; - return SkiaPipeline::shouldForceDither(); -} - void SkiaVulkanPipeline::onContextDestroyed() { if (mVkSurface) { vulkanManager().destroySurface(mVkSurface); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index cce5468e68f0..284cde537ec0 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -63,8 +63,6 @@ public: protected: void onContextDestroyed() override; - bool shouldForceDither() const override; - private: renderthread::VulkanManager& vulkanManager(); renderthread::VulkanSurface* mVkSurface = nullptr; diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index 2b7fa0420cef..10f456745147 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -453,9 +453,15 @@ VulkanSurface::NativeBufferInfo* VulkanSurface::dequeueNativeBuffer() { VulkanSurface::NativeBufferInfo* bufferInfo = &mNativeBuffers[idx]; if (bufferInfo->skSurface.get() == nullptr) { + SkSurfaceProps surfaceProps; + if (mWindowInfo.colorMode != ColorMode::Default) { + surfaceProps = SkSurfaceProps(SkSurfaceProps::kAlwaysDither_Flag | surfaceProps.flags(), + surfaceProps.pixelGeometry()); + } bufferInfo->skSurface = SkSurface::MakeFromAHardwareBuffer( mGrContext, ANativeWindowBuffer_getHardwareBuffer(bufferInfo->buffer.get()), - kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, nullptr, /*from_window=*/true); + kTopLeft_GrSurfaceOrigin, mWindowInfo.colorspace, &surfaceProps, + /*from_window=*/true); if (bufferInfo->skSurface.get() == nullptr) { ALOGE("SkSurface::MakeFromAHardwareBuffer failed"); mNativeWindow->cancelBuffer(mNativeWindow.get(), buffer, @@ -545,16 +551,6 @@ void VulkanSurface::setColorSpace(sk_sp<SkColorSpace> colorSpace) { } } -bool VulkanSurface::isBeyond8Bit() const { - switch (mWindowInfo.bufferFormat) { - case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM: - case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: - return true; - default: - return false; - } -} - } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/VulkanSurface.h b/libs/hwui/renderthread/VulkanSurface.h index d3266af81437..6f5280105e55 100644 --- a/libs/hwui/renderthread/VulkanSurface.h +++ b/libs/hwui/renderthread/VulkanSurface.h @@ -49,8 +49,6 @@ public: void setColorSpace(sk_sp<SkColorSpace> colorSpace); const SkM44& getPixelSnapMatrix() const { return mWindowInfo.pixelSnapMatrix; } - bool isBeyond8Bit() const; - private: /* * All structs/methods in this private section are specifically for use by the VulkanManager diff --git a/media/jni/android_media_MediaCodecLinearBlock.h b/media/jni/android_media_MediaCodecLinearBlock.h index c7530207d1fa..060abfdc1ee5 100644 --- a/media/jni/android_media_MediaCodecLinearBlock.h +++ b/media/jni/android_media_MediaCodecLinearBlock.h @@ -44,12 +44,19 @@ struct JMediaCodecLinearBlock { std::shared_ptr<C2Buffer> toC2Buffer(size_t offset, size_t size) const { if (mBuffer) { + // TODO: if returned C2Buffer is different from mBuffer, we should + // find a way to connect the life cycle between this C2Buffer and + // mBuffer. if (mBuffer->data().type() != C2BufferData::LINEAR) { return nullptr; } C2ConstLinearBlock block = mBuffer->data().linearBlocks().front(); if (offset == 0 && size == block.capacity()) { - return mBuffer; + // Let C2Buffer be new one to queue to MediaCodec. It will allow + // the related input slot to be released by onWorkDone from C2 + // Component. Currently, the life cycle of mBuffer should be + // protected by different flows. + return std::make_shared<C2Buffer>(*mBuffer); } std::shared_ptr<C2Buffer> buffer = diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index d87abb98ebde..ebfb86d4d6ba 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -34,7 +34,7 @@ <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string> <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_watch_single_device">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string> + <string name="summary_watch_single_device">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> <!-- ================= DEVICE_PROFILE_GLASSES ================= --> @@ -48,7 +48,7 @@ <string name="summary_glasses_multi_device">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string> <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] --> - <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string> + <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string> <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= --> @@ -59,7 +59,7 @@ <string name="helper_title_app_streaming">Cross-device services</string> <!-- Description of the helper dialog for APP_STREAMING profile. [CHAR LIMIT=NONE] --> - <string name="helper_summary_app_streaming"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_type" example="Chromebook">%2$s</xliff:g> to stream apps between your devices</string> + <string name="helper_summary_app_streaming"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="display_name" example="Chromebook">%2$s</xliff:g> to stream apps between your devices</string> <!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= --> @@ -81,7 +81,7 @@ <string name="helper_title_computer">Google Play services</string> <!-- Description of the helper dialog for COMPUTER profile. [CHAR LIMIT=NONE] --> - <string name="helper_summary_computer"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_type" example="Chromebook">%2$s</xliff:g> to access your phone\u2019s photos, media, and notifications</string> + <string name="helper_summary_computer"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="display_name" example="Chromebook">%2$s</xliff:g> to access your phone\u2019s photos, media, and notifications</string> <!-- ================= DEVICE_PROFILE_NEARBY_DEVICE_STREAMING ================= --> diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index e9b2e1041c22..3e652517270d 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -117,6 +117,8 @@ <string name="get_dialog_sign_in_type_username_separator" translatable="false">" • "</string> <!-- This text is followed by a list of one or more options. [CHAR LIMIT=80] --> <string name="get_dialog_title_sign_in_options">Sign-in options</string> + <!-- Button label for viewing the full information about an account. [CHAR LIMIT=80] --> + <string name="button_label_view_more">View more</string> <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] --> <string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string> <!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] --> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index dd4419bc6540..e53e956c317c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -65,8 +65,8 @@ class CredentialManagerRepo( ) val originName: String? = when (requestInfo?.type) { - RequestInfo.TYPE_CREATE -> requestInfo?.createCredentialRequest?.origin - RequestInfo.TYPE_GET -> requestInfo?.getCredentialRequest?.origin + RequestInfo.TYPE_CREATE -> requestInfo.createCredentialRequest?.origin + RequestInfo.TYPE_GET -> requestInfo.getCredentialRequest?.origin else -> null } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 7bf1d1993906..ca891294576b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -245,7 +245,7 @@ class GetFlowUtils { userName = credentialEntry.username.toString(), displayName = credentialEntry.displayName?.toString(), icon = credentialEntry.icon.loadDrawable(context), - shouldTintIcon = credentialEntry.isDefaultIcon ?: false, + shouldTintIcon = credentialEntry.isDefaultIcon, lastUsedTimeMillis = credentialEntry.lastUsedTime, isAutoSelectable = credentialEntry.isAutoSelectAllowed && credentialEntry.autoSelectAllowedFromOption, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt index 307d95313ec8..10a75d436a5b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt @@ -316,7 +316,7 @@ fun ModalBottomSheetLayout( rememberModalBottomSheetState(Hidden), sheetShape: Shape = MaterialTheme.shapes.large, sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, - sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface, + sheetBackgroundColor: Color, sheetContentColor: Color = contentColorFor(sheetBackgroundColor), content: @Composable () -> Unit ) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 7a720b1e858b..2dba2ab6777c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -32,7 +32,6 @@ import androidx.compose.material.icons.outlined.Lock import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.TopAppBar @@ -52,6 +51,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection @@ -80,6 +80,7 @@ fun Entry( /** If true, draws a trailing lock icon. */ isLockedAuthEntry: Boolean = false, enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, ) { val iconPadding = Modifier.wrapContentSize().padding( // Horizontal padding should be 16dp, but the suggestion chip itself @@ -104,7 +105,11 @@ fun Entry( ) { // Apply weight so that the trailing icon can always show. Column(modifier = Modifier.wrapContentHeight().fillMaxWidth().weight(1f)) { - SmallTitleText(text = entryHeadlineText, enforceOneLine = enforceOneLine) + SmallTitleText( + text = entryHeadlineText, + enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, + ) if (passwordValue != null) { Row( modifier = Modifier.fillMaxWidth().padding(top = 4.dp), @@ -142,10 +147,18 @@ fun Entry( ) } } else if (entrySecondLineText != null) { - BodySmallText(text = entrySecondLineText, enforceOneLine = enforceOneLine) + BodySmallText( + text = entrySecondLineText, + enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, + ) } if (entryThirdLineText != null) { - BodySmallText(text = entryThirdLineText, enforceOneLine = enforceOneLine) + BodySmallText( + text = entryThirdLineText, + enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, + ) } } if (isLockedAuthEntry) { @@ -155,7 +168,7 @@ fun Entry( // Decorative purpose only. contentDescription = null, modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, ) } } @@ -169,7 +182,7 @@ fun Entry( Icon( modifier = iconSize, bitmap = iconImageBitmap, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -193,7 +206,7 @@ fun Entry( Icon( modifier = iconSize, imageVector = iconImageVector, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -205,7 +218,7 @@ fun Entry( Icon( modifier = iconSize, painter = iconPainter, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -217,9 +230,8 @@ fun Entry( border = null, colors = SuggestionChipDefaults.suggestionChipColors( containerColor = LocalAndroidColorScheme.current.colorSurfaceContainerHigh, - // TODO: remove? - labelColor = MaterialTheme.colorScheme.onSurfaceVariant, - iconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + labelColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + iconContentColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant, ), ) } @@ -282,7 +294,7 @@ fun PasskeyBenefitRow( Icon( modifier = Modifier.size(24.dp), painter = leadingIconPainter, - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -341,7 +353,7 @@ fun MoreOptionTopAppBar( R.string.accessibility_back_arrow_button ), modifier = Modifier.size(24.dp).autoMirrored(), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt index 358122809985..14bf4f23384b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt @@ -20,20 +20,20 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @Composable fun CredentialListSectionHeader(text: String) { - InternalSectionHeader(text, MaterialTheme.colorScheme.onSurfaceVariant) + InternalSectionHeader(text, LocalAndroidColorScheme.current.colorOnSurfaceVariant) } @Composable fun MoreAboutPasskeySectionHeader(text: String) { - InternalSectionHeader(text, MaterialTheme.colorScheme.onSurface) + InternalSectionHeader(text, LocalAndroidColorScheme.current.colorOnSurface) } @Composable diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt index 22871bcbe767..6b46636964e4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt @@ -22,8 +22,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme /** * The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X". @@ -35,7 +37,7 @@ fun HeadlineText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = MaterialTheme.colorScheme.onSurface, + color = LocalAndroidColorScheme.current.colorOnSurface, textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineSmall, ) @@ -49,7 +51,7 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) } @@ -58,14 +60,20 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) { * Body-small typography; on-surface-variant color. */ @Composable -fun BodySmallText(text: String, modifier: Modifier = Modifier, enforceOneLine: Boolean = false) { +fun BodySmallText( + text: String, + modifier: Modifier = Modifier, + enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, +) { Text( modifier = modifier.wrapContentSize(), text = text, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, style = MaterialTheme.typography.bodySmall, overflow = TextOverflow.Ellipsis, - maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE + maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, + onTextLayout = onTextLayout, ) } @@ -77,7 +85,7 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = MaterialTheme.colorScheme.onSurface, + color = LocalAndroidColorScheme.current.colorOnSurface, style = MaterialTheme.typography.titleLarge, ) } @@ -86,14 +94,20 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) { * Title-small typography; on-surface color. */ @Composable -fun SmallTitleText(text: String, modifier: Modifier = Modifier, enforceOneLine: Boolean = false) { +fun SmallTitleText( + text: String, + modifier: Modifier = Modifier, + enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, +) { Text( modifier = modifier.wrapContentSize(), text = text, - color = MaterialTheme.colorScheme.onSurface, + color = LocalAndroidColorScheme.current.colorOnSurface, style = MaterialTheme.typography.titleSmall, overflow = TextOverflow.Ellipsis, - maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE + maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, + onTextLayout = onTextLayout, ) } @@ -145,7 +159,7 @@ fun LargeLabelTextOnSurfaceVariant(text: String, modifier: Modifier = Modifier) modifier = modifier.wrapContentSize(), text = text, textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, style = MaterialTheme.typography.labelLarge, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 648d83268541..66d7db896247 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.filled.Add @@ -67,6 +66,7 @@ import com.android.credentialmanager.common.ui.SheetContainerCard import com.android.credentialmanager.common.ui.PasskeyBenefitRow import com.android.credentialmanager.common.ui.HeadlineText import com.android.credentialmanager.logging.CreateCredentialEvent +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import com.android.internal.logging.UiEventLogger.UiEventEnum @Composable @@ -559,7 +559,7 @@ fun CreationSelectionCard( item { Divider( thickness = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant, + color = LocalAndroidColorScheme.current.colorOutlineVariant, modifier = Modifier.padding(vertical = 16.dp) ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 6d7ecd70eb0b..c1ea1d8d1746 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -34,11 +34,15 @@ import androidx.compose.material3.Divider import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.CredentialSelectorViewModel @@ -158,6 +162,7 @@ fun PrimarySelectionCard( onMoreOptionSelected: () -> Unit, onLog: @Composable (UiEventEnum) -> Unit, ) { + val showMoreForTruncatedEntry = remember { mutableStateOf(false) } val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList val authenticationEntryList = providerDisplayInfo.authenticationEntryList @@ -209,6 +214,8 @@ fun PrimarySelectionCard( Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { val usernameForCredentialSize = sortedUserNameToCredentialEntryList.size val authenticationEntrySize = authenticationEntryList.size + // If true, render a view more button for the single truncated entry on the + // front page. // Show max 4 entries in this primary page if (usernameForCredentialSize + authenticationEntrySize <= 4) { sortedUserNameToCredentialEntryList.forEach { @@ -216,6 +223,9 @@ fun PrimarySelectionCard( credentialEntryInfo = it.sortedCredentialEntryList.first(), onEntrySelected = onEntrySelected, enforceOneLine = true, + onTextLayout = { + showMoreForTruncatedEntry.value = it.hasVisualOverflow + } ) } authenticationEntryList.forEach { @@ -269,6 +279,13 @@ fun PrimarySelectionCard( onMoreOptionSelected ) } + } else if (showMoreForTruncatedEntry.value) { + { + ActionButton( + stringResource(R.string.button_label_view_more), + onMoreOptionSelected + ) + } } else null, rightButton = if (activeEntry != null) { // Only one sign-in options exist { @@ -438,6 +455,7 @@ fun CredentialEntryRow( credentialEntryInfo: CredentialEntryInfo, onEntrySelected: (BaseEntry) -> Unit, enforceOneLine: Boolean = false, + onTextLayout: (TextLayoutResult) -> Unit = {}, ) { Entry( onClick = { onEntrySelected(credentialEntryInfo) }, @@ -463,6 +481,7 @@ fun CredentialEntryRow( ) }, enforceOneLine = enforceOneLine, + onTextLayout = onTextLayout, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt index 8928e1869838..a33904d30393 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt @@ -42,6 +42,9 @@ val LocalAndroidColorScheme = class AndroidColorScheme internal constructor(context: Context) { val colorSurfaceBright = getColor(context, R.attr.materialColorSurfaceBright) val colorSurfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh) + val colorOutlineVariant = getColor(context, R.attr.materialColorOutlineVariant) + val colorOnSurface = getColor(context, R.attr.materialColorOnSurface) + val colorOnSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant) companion object { fun getColor(context: Context, attr: Int): Color { diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverLogging.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverLogging.java new file mode 100644 index 000000000000..5326e73a3f82 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverLogging.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 com.android.settingslib.fuelgauge; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Utilities related to battery saver logging. + */ +public final class BatterySaverLogging { + /** + * Record the reason while enabling power save mode manually. + * See {@link SaverManualEnabledReason} for all available states. + */ + public static final String EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED_REASON = + "extra_power_save_mode_manual_enabled_reason"; + + /** Broadcast action to record battery saver manual enabled reason. */ + public static final String ACTION_SAVER_MANUAL_ENABLED_REASON = + "com.android.settingslib.fuelgauge.ACTION_SAVER_MANUAL_ENABLED_REASON"; + + /** An interface for the battery saver manual enable reason. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SAVER_ENABLED_UNKNOWN, SAVER_ENABLED_CONFIRMATION, SAVER_ENABLED_VOICE, + SAVER_ENABLED_SETTINGS, SAVER_ENABLED_QS, SAVER_ENABLED_LOW_WARNING, + SAVER_ENABLED_SEVERE_WARNING}) + public @interface SaverManualEnabledReason {} + + public static final int SAVER_ENABLED_UNKNOWN = 0; + public static final int SAVER_ENABLED_CONFIRMATION = 1; + public static final int SAVER_ENABLED_VOICE = 2; + public static final int SAVER_ENABLED_SETTINGS = 3; + public static final int SAVER_ENABLED_QS = 4; + public static final int SAVER_ENABLED_LOW_WARNING = 5; + public static final int SAVER_ENABLED_SEVERE_WARNING = 6; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java index 52f3111d967c..a3db6d7e17ff 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java @@ -16,6 +16,10 @@ package com.android.settingslib.fuelgauge; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.ACTION_SAVER_MANUAL_ENABLED_REASON; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED_REASON; +import static com.android.settingslib.fuelgauge.BatterySaverLogging.SaverManualEnabledReason; + import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -145,7 +149,8 @@ public class BatterySaverUtils { && Global.getInt(cr, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0) == 0 && Secure.getInt(cr, Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 0) == 0) { - showAutoBatterySaverSuggestion(context, confirmationExtras); + sendSystemUiBroadcast(context, ACTION_SHOW_AUTO_SAVER_SUGGESTION, + confirmationExtras); } } @@ -175,21 +180,23 @@ public class BatterySaverUtils { // Already shown. return false; } - context.sendBroadcast( - getSystemUiBroadcast(ACTION_SHOW_START_SAVER_CONFIRMATION, extras)); + sendSystemUiBroadcast(context, ACTION_SHOW_START_SAVER_CONFIRMATION, extras); return true; } - private static void showAutoBatterySaverSuggestion(Context context, Bundle extras) { - context.sendBroadcast(getSystemUiBroadcast(ACTION_SHOW_AUTO_SAVER_SUGGESTION, extras)); + private static void recordBatterySaverEnabledReason(Context context, + @SaverManualEnabledReason int reason) { + final Bundle enabledReasonExtras = new Bundle(1); + enabledReasonExtras.putInt(EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED_REASON, reason); + sendSystemUiBroadcast(context, ACTION_SAVER_MANUAL_ENABLED_REASON, enabledReasonExtras); } - private static Intent getSystemUiBroadcast(String action, Bundle extras) { - final Intent i = new Intent(action); - i.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - i.setPackage(SYSUI_PACKAGE); - i.putExtras(extras); - return i; + private static void sendSystemUiBroadcast(Context context, String action, Bundle extras) { + final Intent intent = new Intent(action); + intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + intent.setPackage(SYSUI_PACKAGE); + intent.putExtras(extras); + context.sendBroadcast(intent); } private static void setBatterySaverConfirmationAcknowledged(Context context) { diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 59cd7a051fad..a93cd62e6301 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -43,6 +43,8 @@ <bool name="def_install_non_market_apps">false</bool> <!-- 0 == off, 3 == on --> <integer name="def_location_mode">3</integer> + <!-- 0 == off, 1 == on--> + <integer name="def_paired_device_location_mode">1</integer> <bool name="assisted_gps_enabled">true</bool> <bool name="def_netstats_enabled">true</bool> <bool name="def_usb_mass_storage_enabled">true</bool> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index e50f52229a16..41ce58eb7b4e 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -103,5 +103,7 @@ public class GlobalSettings { Settings.Global.Wearable.UPGRADE_DATA_MIGRATION_STATUS, Settings.Global.HDR_CONVERSION_MODE, Settings.Global.HDR_FORCE_CONVERSION_TYPE, + Settings.Global.Wearable.RTL_SWIPE_TO_DISMISS_ENABLED_DEV, + Settings.Global.Wearable.REDUCE_MOTION, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index d5386c1d75ac..a1c01723ad55 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -285,7 +285,6 @@ public class GlobalSettingsValidators { })); VALIDATORS.put(Global.Wearable.MUTE_WHEN_OFF_BODY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.SIDE_BUTTON, BOOLEAN_VALIDATOR); - VALIDATORS.put(Global.Wearable.BUTTON_SET, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.ANDROID_WEAR_VERSION, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Global.Wearable.SYSTEM_CAPABILITIES, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Global.Wearable.SYSTEM_EDITION, ANY_INTEGER_VALIDATOR); @@ -345,6 +344,7 @@ public class GlobalSettingsValidators { String.valueOf(Global.Wearable.HFP_CLIENT_DISABLED) })); VALIDATORS.put(Global.Wearable.COMPANION_OS_VERSION, ANY_INTEGER_VALIDATOR); + VALIDATORS.put(Global.Wearable.COMPANION_APP_NAME, ANY_STRING_VALIDATOR); VALIDATORS.put(Global.Wearable.ENABLE_ALL_LANGUAGES, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.OEM_SETUP_VERSION, ANY_INTEGER_VALIDATOR); VALIDATORS.put( @@ -404,16 +404,6 @@ public class GlobalSettingsValidators { VALIDATORS.put(Global.Wearable.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.BEDTIME_MODE, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.BEDTIME_HARD_MODE, BOOLEAN_VALIDATOR); - VALIDATORS.put( - Global.Wearable.EARLY_UPDATES_STATUS, - new DiscreteValueValidator( - new String[] { - String.valueOf(Global.Wearable.EARLY_UPDATES_STATUS_NOT_STARTED), - String.valueOf(Global.Wearable.EARLY_UPDATES_STATUS_STARTED), - String.valueOf(Global.Wearable.EARLY_UPDATES_STATUS_SUCCESS), - String.valueOf(Global.Wearable.EARLY_UPDATES_STATUS_SKIPPED), - String.valueOf(Global.Wearable.EARLY_UPDATES_STATUS_ABORTED), - })); VALIDATORS.put(Global.Wearable.DYNAMIC_COLOR_THEME_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.SCREENSHOT_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.UPGRADE_DATA_MIGRATION_STATUS, @@ -423,5 +413,22 @@ public class GlobalSettingsValidators { String.valueOf(Global.Wearable.UPGRADE_DATA_MIGRATION_PENDING), String.valueOf(Global.Wearable.UPGRADE_DATA_MIGRATION_DONE) })); + VALIDATORS.put(Global.Wearable.DISABLE_AOD_WHILE_PLUGGED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.Wearable.NETWORK_LOCATION_OPT_IN, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_STATUS, + new InclusiveIntegerRangeValidator( + Global.Wearable.PHONE_SWITCHING_STATUS_NOT_STARTED, + Global.Wearable.PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_SUCCESS)); + VALIDATORS.put(Global.Wearable.REDUCE_MOTION, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.Wearable.RTL_SWIPE_TO_DISMISS_ENABLED_DEV, BOOLEAN_VALIDATOR); + VALIDATORS.put( + Global.Wearable.TETHER_CONFIG_STATE, + new DiscreteValueValidator( + new String[] { + String.valueOf(Global.Wearable.TETHERED_CONFIG_UNKNOWN), + String.valueOf(Global.Wearable.TETHERED_CONFIG_STANDALONE), + String.valueOf(Global.Wearable.TETHERED_CONFIG_TETHERED) + })); + VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_SUPPORTED, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index b0a19270018a..284b06b86cb6 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3748,7 +3748,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 217; + private static final int SETTINGS_VERSION = 218; private final int mUserId; @@ -5334,74 +5334,73 @@ public class SettingsProvider extends ContentProvider { if (currentVersion == 203) { // Version 203: initialize entries migrated from wear settings provide. - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.HAS_PAY_TOKENS, false); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.GMS_CHECKIN_TIMEOUT_MIN, 6); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.HOTWORD_DETECTION_ENABLED, getContext() .getResources() .getBoolean(R.bool.def_wearable_hotwordDetectionEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.SMART_REPLIES_ENABLED, true); Setting locationMode = getSecureSettingsLocked(userId).getSettingLocked(Secure.LOCATION_MODE); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.OBTAIN_PAIRED_DEVICE_LOCATION, !locationMode.isNull() && !Integer.toString(Secure.LOCATION_MODE_OFF) .equals(locationMode.getValue())); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.PHONE_PLAY_STORE_AVAILABILITY, Global.Wearable.PHONE_PLAY_STORE_AVAILABILITY_UNKNOWN); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.BUG_REPORT, "user".equals(Build.TYPE) // is user build? ? Global.Wearable.BUG_REPORT_DISABLED : Global.Wearable.BUG_REPORT_ENABLED); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.SMART_ILLUMINATE_ENABLED, getContext() .getResources() .getBoolean(R.bool.def_wearable_smartIlluminateEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.CLOCKWORK_AUTO_TIME, Global.Wearable.SYNC_TIME_FROM_PHONE); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE, Global.Wearable.SYNC_TIME_ZONE_FROM_PHONE); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.CLOCKWORK_24HR_TIME, false); - initGlobalSettingsDefaultValForWearLocked(Global.Wearable.AUTO_WIFI, true); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked(Global.Wearable.AUTO_WIFI, true); + initGlobalSettingsDefaultValLocked( Global.Wearable.WIFI_POWER_SAVE, getContext() .getResources() .getInteger( R.integer .def_wearable_offChargerWifiUsageLimitMinutes)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.ALT_BYPASS_WIFI_REQUIREMENT_TIME_MILLIS, 0L); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.SETUP_SKIPPED, Global.Wearable.SETUP_SKIPPED_UNKNOWN); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.LAST_CALL_FORWARD_ACTION, Global.Wearable.CALL_FORWARD_NO_LAST_ACTION); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.MUTE_WHEN_OFF_BODY_ENABLED, getContext() .getResources() .getBoolean(R.bool.def_wearable_muteWhenOffBodyEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.WEAR_OS_VERSION_STRING, ""); - initGlobalSettingsDefaultValForWearLocked(Global.Wearable.BUTTON_SET, false); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.SIDE_BUTTON, getContext() .getResources() .getBoolean(R.bool.def_wearable_sideButtonPresent)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.ANDROID_WEAR_VERSION, Long.parseLong( getContext() @@ -5410,55 +5409,55 @@ public class SettingsProvider extends ContentProvider { final int editionGlobal = 1; final int editionLocal = 2; boolean isLe = getContext().getPackageManager().hasSystemFeature("cn.google"); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.SYSTEM_EDITION, isLe ? editionLocal : editionGlobal); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.SYSTEM_CAPABILITIES, getWearSystemCapabilities(isLe)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.WEAR_PLATFORM_MR_NUMBER, SystemProperties.getInt("ro.cw_build.platform_mr", 0)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.MOBILE_SIGNAL_DETECTOR, getContext() .getResources() .getBoolean(R.bool.def_wearable_mobileSignalDetectorAllowed)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.AMBIENT_ENABLED, getContext() .getResources() .getBoolean(R.bool.def_wearable_ambientEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.AMBIENT_TILT_TO_WAKE, getContext() .getResources() .getBoolean(R.bool.def_wearable_tiltToWakeEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.AMBIENT_LOW_BIT_ENABLED_DEV, false); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.AMBIENT_TOUCH_TO_WAKE, getContext() .getResources() .getBoolean(R.bool.def_wearable_touchToWakeEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.AMBIENT_TILT_TO_BRIGHT, getContext() .getResources() .getBoolean(R.bool.def_wearable_tiltToBrightEnabled)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Global.Wearable.DECOMPOSABLE_WATCHFACE, false); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.AMBIENT_FORCE_WHEN_DOCKED, SystemProperties.getBoolean("ro.ambient.force_when_docked", false)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.AMBIENT_LOW_BIT_ENABLED, SystemProperties.getBoolean("ro.ambient.low_bit_enabled", false)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.AMBIENT_PLUGGED_TIMEOUT_MIN, SystemProperties.getInt("ro.ambient.plugged_timeout_min", -1)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.PAIRED_DEVICE_OS_TYPE, Settings.Global.Wearable.PAIRED_DEVICE_OS_TYPE_UNKNOWN); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.USER_HFP_CLIENT_SETTING, Settings.Global.Wearable.HFP_CLIENT_UNSET); Setting disabledProfileSetting = @@ -5468,7 +5467,7 @@ public class SettingsProvider extends ContentProvider { disabledProfileSetting.isNull() ? 0 : Long.parseLong(disabledProfileSetting.getValue()); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.COMPANION_OS_VERSION, Settings.Global.Wearable.COMPANION_OS_VERSION_UNDEFINED); final boolean defaultBurnInProtectionEnabled = @@ -5482,17 +5481,17 @@ public class SettingsProvider extends ContentProvider { .config_enableBurnInProtection); final boolean forceBurnInProtection = SystemProperties.getBoolean("persist.debug.force_burn_in", false); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.BURN_IN_PROTECTION_ENABLED, defaultBurnInProtectionEnabled || forceBurnInProtection); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.CLOCKWORK_SYSUI_PACKAGE, getContext() .getResources() .getString( com.android.internal.R.string.config_wearSysUiPackage)); - initGlobalSettingsDefaultValForWearLocked( + initGlobalSettingsDefaultValLocked( Settings.Global.Wearable.CLOCKWORK_SYSUI_MAIN_ACTIVITY, getContext() .getResources() @@ -5622,63 +5621,16 @@ public class SettingsProvider extends ContentProvider { currentVersion = 210; } if (currentVersion == 210) { - final SettingsState secureSettings = getSecureSettingsLocked(userId); - final Setting currentSetting = secureSettings.getSettingLocked( - Secure.STATUS_BAR_SHOW_VIBRATE_ICON); - if (currentSetting.isNull()) { - final int defaultValueVibrateIconEnabled = getContext().getResources() - .getInteger(R.integer.def_statusBarVibrateIconEnabled); - secureSettings.insertSettingOverrideableByRestoreLocked( - Secure.STATUS_BAR_SHOW_VIBRATE_ICON, - String.valueOf(defaultValueVibrateIconEnabled), - null /* tag */, true /* makeDefault */, - SettingsState.SYSTEM_PACKAGE_NAME); - } + // Unused. Moved to version 217. currentVersion = 211; } if (currentVersion == 211) { - // Version 211: Set default value for - // Secure#LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS - final SettingsState secureSettings = getSecureSettingsLocked(userId); - final Setting lockScreenUnseenSetting = secureSettings - .getSettingLocked(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS); - if (lockScreenUnseenSetting.isNull()) { - final boolean defSetting = getContext().getResources() - .getBoolean(R.bool.def_lock_screen_show_only_unseen_notifications); - secureSettings.insertSettingOverrideableByRestoreLocked( - Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - defSetting ? "1" : "0", - null /* tag */, - true /* makeDefault */, - SettingsState.SYSTEM_PACKAGE_NAME); - } - + // Unused. Moved to version 217. currentVersion = 212; } if (currentVersion == 212) { - final SettingsState globalSettings = getGlobalSettingsLocked(); - final SettingsState secureSettings = getSecureSettingsLocked(userId); - - final Setting bugReportInPowerMenu = globalSettings.getSettingLocked( - Global.BUGREPORT_IN_POWER_MENU); - - if (!bugReportInPowerMenu.isNull()) { - Slog.i(LOG_TAG, "Setting bugreport_in_power_menu to " - + bugReportInPowerMenu.getValue() + " in Secure settings."); - secureSettings.insertSettingLocked( - Secure.BUGREPORT_IN_POWER_MENU, - bugReportInPowerMenu.getValue(), null /* tag */, - false /* makeDefault */, SettingsState.SYSTEM_PACKAGE_NAME); - - // set global bug_report_in_power_menu setting to null since it's deprecated - Slog.i(LOG_TAG, "Setting bugreport_in_power_menu to null" - + " in Global settings since it's deprecated."); - globalSettings.insertSettingLocked( - Global.BUGREPORT_IN_POWER_MENU, null /* value */, null /* tag */, - true /* makeDefault */, SettingsState.SYSTEM_PACKAGE_NAME); - } - + // Unused. Moved to version 217. currentVersion = 213; } @@ -5804,6 +5756,89 @@ public class SettingsProvider extends ContentProvider { currentVersion = 217; } + if (currentVersion == 217) { + // Version 217: merge and rebase wear settings init logic. + + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final SettingsState globalSettings = getGlobalSettingsLocked(); + + // Following init logic is moved from version 210 to this version in order to + // resolve version conflict with wear branch. + final Setting currentSetting = secureSettings.getSettingLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON); + if (currentSetting.isNull()) { + final int defaultValueVibrateIconEnabled = getContext().getResources() + .getInteger(R.integer.def_statusBarVibrateIconEnabled); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + String.valueOf(defaultValueVibrateIconEnabled), + null /* tag */, true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } + + // Set default value for Secure#LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS + // Following init logic is moved from version 211 to this version in order to + // resolve version conflict with wear branch. + final Setting lockScreenUnseenSetting = secureSettings + .getSettingLocked(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS); + if (lockScreenUnseenSetting.isNull()) { + final boolean defSetting = getContext().getResources() + .getBoolean(R.bool.def_lock_screen_show_only_unseen_notifications); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + defSetting ? "1" : "0", + null /* tag */, + true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } + + // Following init logic is moved from version 212 to this version in order to + // resolve version conflict with wear branch. + final Setting bugReportInPowerMenu = globalSettings.getSettingLocked( + Global.BUGREPORT_IN_POWER_MENU); + + if (!bugReportInPowerMenu.isNull()) { + Slog.i(LOG_TAG, "Setting bugreport_in_power_menu to " + + bugReportInPowerMenu.getValue() + " in Secure settings."); + secureSettings.insertSettingLocked( + Secure.BUGREPORT_IN_POWER_MENU, + bugReportInPowerMenu.getValue(), null /* tag */, + false /* makeDefault */, SettingsState.SYSTEM_PACKAGE_NAME); + + // set global bug_report_in_power_menu setting to null since it's deprecated + Slog.i(LOG_TAG, "Setting bugreport_in_power_menu to null" + + " in Global settings since it's deprecated."); + globalSettings.insertSettingLocked( + Global.BUGREPORT_IN_POWER_MENU, null /* value */, null /* tag */, + true /* makeDefault */, SettingsState.SYSTEM_PACKAGE_NAME); + } + + // Following init logic is rebased from wear OS branch. + // Initialize default value of tether configuration to unknown. + initGlobalSettingsDefaultValLocked( + Settings.Global.Wearable.TETHER_CONFIG_STATE, + Global.Wearable.TETHERED_CONFIG_UNKNOWN); + // Init paired device location setting from resources. + initGlobalSettingsDefaultValLocked( + Global.Wearable.OBTAIN_PAIRED_DEVICE_LOCATION, + getContext() + .getResources() + .getInteger(R.integer.def_paired_device_location_mode)); + // Init media packages from resources. + final String mediaControlsPackage = getContext().getResources().getString( + com.android.internal.R.string.config_wearMediaControlsPackage); + final String mediaSessionsPackage = getContext().getResources().getString( + com.android.internal.R.string.config_wearMediaSessionsPackage); + initGlobalSettingsDefaultValLocked( + Global.Wearable.WEAR_MEDIA_CONTROLS_PACKAGE, + mediaControlsPackage); + initGlobalSettingsDefaultValLocked( + Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE, + mediaSessionsPackage); + + currentVersion = 218; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { @@ -5821,19 +5856,19 @@ public class SettingsProvider extends ContentProvider { return currentVersion; } - private void initGlobalSettingsDefaultValForWearLocked(String key, boolean val) { - initGlobalSettingsDefaultValForWearLocked(key, val ? "1" : "0"); + private void initGlobalSettingsDefaultValLocked(String key, boolean val) { + initGlobalSettingsDefaultValLocked(key, val ? "1" : "0"); } - private void initGlobalSettingsDefaultValForWearLocked(String key, int val) { - initGlobalSettingsDefaultValForWearLocked(key, String.valueOf(val)); + private void initGlobalSettingsDefaultValLocked(String key, int val) { + initGlobalSettingsDefaultValLocked(key, String.valueOf(val)); } - private void initGlobalSettingsDefaultValForWearLocked(String key, long val) { - initGlobalSettingsDefaultValForWearLocked(key, String.valueOf(val)); + private void initGlobalSettingsDefaultValLocked(String key, long val) { + initGlobalSettingsDefaultValLocked(key, String.valueOf(val)); } - private void initGlobalSettingsDefaultValForWearLocked(String key, String val) { + private void initGlobalSettingsDefaultValLocked(String key, String val) { final SettingsState globalSettings = getGlobalSettingsLocked(); Setting currentSetting = globalSettings.getSettingLocked(key); if (currentSetting.isNull()) { diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 19f1a86ec90c..a202e1614b67 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -627,7 +627,6 @@ public class SettingsBackupTest { Settings.Global.Wearable.STEM_3_DATA, Settings.Global.Wearable.STEM_3_DEFAULT_DATA, Settings.Global.Wearable.WEAR_OS_VERSION_STRING, - Settings.Global.Wearable.BUTTON_SET, Settings.Global.Wearable.SIDE_BUTTON, Settings.Global.Wearable.ANDROID_WEAR_VERSION, Settings.Global.Wearable.SYSTEM_CAPABILITIES, @@ -643,6 +642,7 @@ public class SettingsBackupTest { Settings.Global.Wearable.PAIRED_DEVICE_OS_TYPE, Settings.Global.Wearable.COMPANION_BLE_ROLE, Settings.Global.Wearable.COMPANION_NAME, + Settings.Global.Wearable.COMPANION_APP_NAME, Settings.Global.Wearable.USER_HFP_CLIENT_SETTING, Settings.Global.Wearable.COMPANION_OS_VERSION, Settings.Global.Wearable.ENABLE_ALL_LANGUAGES, @@ -662,13 +662,21 @@ public class SettingsBackupTest { Settings.Global.Wearable.SCREEN_UNLOCK_SOUND_ENABLED, Settings.Global.Wearable.BEDTIME_MODE, Settings.Global.Wearable.BEDTIME_HARD_MODE, - Settings.Global.Wearable.EARLY_UPDATES_STATUS, Settings.Global.Wearable.RSB_WAKE_ENABLED, Settings.Global.Wearable.LOCK_SCREEN_STATE, Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED, Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE, Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED, - Settings.Global.Wearable.SCREENSHOT_ENABLED); + Settings.Global.Wearable.SCREENSHOT_ENABLED, + Settings.Global.Wearable.DISABLE_AOD_WHILE_PLUGGED, + Settings.Global.Wearable.NETWORK_LOCATION_OPT_IN, + Settings.Global.Wearable.CUSTOM_COLOR_FOREGROUND, + Settings.Global.Wearable.CUSTOM_COLOR_BACKGROUND, + Settings.Global.Wearable.PHONE_SWITCHING_STATUS, + Settings.Global.Wearable.TETHER_CONFIG_STATE, + Settings.Global.Wearable.PHONE_SWITCHING_SUPPORTED, + Settings.Global.Wearable.WEAR_MEDIA_CONTROLS_PACKAGE, + Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE); private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS = newHashSet( diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 3007d4a79d13..7a1d9a3ad025 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -156,9 +156,10 @@ android_library { "WifiTrackerLib", "WindowManager-Shell", "SystemUIAnimationLib", + "SystemUICommon", + "SystemUICustomizationLib", "SystemUIPluginLib", "SystemUISharedLib", - "SystemUICustomizationLib", "SystemUI-statsd", "SettingsLib", "androidx.core_core-ktx", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 36a0b5dd72b8..dabb5780621c 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -347,15 +347,6 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> - <!-- Intent Chooser --> - <permission - android:name="android.permission.ADD_CHOOSER_PINS" - android:protectionLevel="signature" /> - <uses-permission android:name="android.permission.ADD_CHOOSER_PINS" /> - <permission - android:name="android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" - android:protectionLevel="signature" /> - <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/preferences_action_bar.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/preferences_action_bar.xml new file mode 100644 index 000000000000..1d67066028be --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/preferences_action_bar.xml @@ -0,0 +1,29 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/action_bar_title" + style="@style/TextAppearance.AppCompat.Title" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:maxLines="5"/> +</LinearLayout> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java index 02d279fa4962..5ed450abede5 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.accessibilitymenu.activity; +import android.app.ActionBar; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -24,6 +25,7 @@ import android.net.Uri; import android.os.Bundle; import android.provider.Browser; import android.provider.Settings; +import android.widget.TextView; import android.view.View; import androidx.annotation.Nullable; @@ -46,6 +48,13 @@ public class A11yMenuSettingsActivity extends FragmentActivity { .beginTransaction() .replace(android.R.id.content, new A11yMenuPreferenceFragment()) .commit(); + + ActionBar actionBar = getActionBar(); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setCustomView(R.layout.preferences_action_bar); + ((TextView) findViewById(R.id.action_bar_title)).setText( + getResources().getString(R.string.accessibility_menu_settings_name) + ); } /** diff --git a/packages/SystemUI/common/.gitignore b/packages/SystemUI/common/.gitignore new file mode 100644 index 000000000000..f9a33dbbcc7e --- /dev/null +++ b/packages/SystemUI/common/.gitignore @@ -0,0 +1,9 @@ +.idea/ +.gradle/ +gradle/ +build/ +gradlew* +local.properties +*.iml +android.properties +buildSrc
\ No newline at end of file diff --git a/packages/SystemUI/common/Android.bp b/packages/SystemUI/common/Android.bp new file mode 100644 index 000000000000..e36ada89b207 --- /dev/null +++ b/packages/SystemUI/common/Android.bp @@ -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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + + name: "SystemUICommon", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + + static_libs: [ + "androidx.core_core-ktx", + ], + + manifest: "AndroidManifest.xml", + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/common/AndroidManifest.xml b/packages/SystemUI/common/AndroidManifest.xml new file mode 100644 index 000000000000..6f757eb67d2e --- /dev/null +++ b/packages/SystemUI/common/AndroidManifest.xml @@ -0,0 +1,21 @@ +<?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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.common"> + +</manifest> diff --git a/packages/SystemUI/common/OWNERS b/packages/SystemUI/common/OWNERS new file mode 100644 index 000000000000..9b8a79e6f3c7 --- /dev/null +++ b/packages/SystemUI/common/OWNERS @@ -0,0 +1,2 @@ +darrellshi@google.com +evanlaird@google.com diff --git a/packages/SystemUI/common/README.md b/packages/SystemUI/common/README.md new file mode 100644 index 000000000000..1cc5277aa83e --- /dev/null +++ b/packages/SystemUI/common/README.md @@ -0,0 +1,5 @@ +# SystemUICommon + +`SystemUICommon` is a module within SystemUI that hosts standalone helper libraries. It is intended to be used by other modules, and therefore should not have other SystemUI dependencies to avoid circular dependencies. + +To maintain the structure of this module, please refrain from adding components at the top level. Instead, add them to specific sub-packages, such as `systemui/common/buffer/`. This will help to keep the module organized and easy to navigate. diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt b/packages/SystemUI/common/src/com/android/systemui/common/buffer/RingBuffer.kt index 4773f54a079e..de49d1c2c5ee 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt +++ b/packages/SystemUI/common/src/com/android/systemui/common/buffer/RingBuffer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.plugins.util +package com.android.systemui.common.buffer import kotlin.math.max diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index fb1c454de70d..e306d4aac398 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -37,6 +37,7 @@ java_library { "error_prone_annotations", "PluginCoreLib", "SystemUIAnimationLib", + "SystemUICommon", ], } 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 3e34885a6d9c..4a6e0b61ecc9 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 @@ -18,7 +18,7 @@ package com.android.systemui.plugins.log import android.os.Trace import android.util.Log -import com.android.systemui.plugins.util.RingBuffer +import com.android.systemui.common.buffer.RingBuffer import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.util.concurrent.ArrayBlockingQueue diff --git a/packages/SystemUI/res-keyguard/drawable/ic_palette.xml b/packages/SystemUI/res-keyguard/drawable/ic_palette.xml new file mode 100644 index 000000000000..cbea369c0236 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_palette.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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,395 112,322Q144,249 199.5,195Q255,141 329.5,110.5Q404,80 489,80Q568,80 639,106.5Q710,133 763.5,180Q817,227 848.5,291.5Q880,356 880,433Q880,541 817,603.5Q754,666 650,666L575,666Q557,666 544,680Q531,694 531,711Q531,738 545.5,757Q560,776 560,801Q560,839 539,859.5Q518,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM247,506Q267,506 282,491Q297,476 297,456Q297,436 282,421Q267,406 247,406Q227,406 212,421Q197,436 197,456Q197,476 212,491Q227,506 247,506ZM373,336Q393,336 408,321Q423,306 423,286Q423,266 408,251Q393,236 373,236Q353,236 338,251Q323,266 323,286Q323,306 338,321Q353,336 373,336ZM587,336Q607,336 622,321Q637,306 637,286Q637,266 622,251Q607,236 587,236Q567,236 552,251Q537,266 537,286Q537,306 552,321Q567,336 587,336ZM718,506Q738,506 753,491Q768,476 768,456Q768,436 753,421Q738,406 718,406Q698,406 683,421Q668,436 668,456Q668,476 683,491Q698,506 718,506ZM480,820Q491,820 495.5,815.5Q500,811 500,801Q500,787 485.5,775Q471,763 471,722Q471,676 501,641Q531,606 577,606L650,606Q726,606 773,561.5Q820,517 820,433Q820,301 720,220.5Q620,140 489,140Q343,140 241.5,238.5Q140,337 140,480Q140,621 239.5,720.5Q339,820 480,820Z"/> +</vector> diff --git a/packages/SystemUI/res/anim/keyguard_settings_popup_ease_out_interpolator.xml b/packages/SystemUI/res/anim/keyguard_settings_popup_ease_out_interpolator.xml new file mode 100644 index 000000000000..8c2937c8c483 --- /dev/null +++ b/packages/SystemUI/res/anim/keyguard_settings_popup_ease_out_interpolator.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. + --> + +<pathInterpolator + xmlns:android="http://schemas.android.com/apk/res/android" + android:controlX1="0.30" + android:controlY1="0.00" + android:controlX2="0.33" + android:controlY2="1.00" /> diff --git a/packages/SystemUI/res/anim/long_press_lock_screen_popup_enter.xml b/packages/SystemUI/res/anim/long_press_lock_screen_popup_enter.xml new file mode 100644 index 000000000000..5fa88224968b --- /dev/null +++ b/packages/SystemUI/res/anim/long_press_lock_screen_popup_enter.xml @@ -0,0 +1,49 @@ +<?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. + --> + +<set + xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + + <scale + android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:duration="200" + android:fromXScale="0.5" + android:fromYScale="0.5" + android:toXScale="1.02" + android:toYScale="1.02" + android:pivotX="50%" + android:pivotY="50%" /> + + <scale + android:interpolator="@anim/keyguard_settings_popup_ease_out_interpolator" + android:startOffset="200" + android:duration="200" + android:fromXScale="1" + android:fromYScale="1" + android:toXScale="0.98" + android:toYScale="0.98" + android:pivotX="50%" + android:pivotY="50%" /> + + <alpha + android:interpolator="@android:anim/linear_interpolator" + android:duration="83" + android:fromAlpha="0" + android:toAlpha="1" /> + +</set> diff --git a/packages/SystemUI/res/anim/long_press_lock_screen_popup_exit.xml b/packages/SystemUI/res/anim/long_press_lock_screen_popup_exit.xml new file mode 100644 index 000000000000..a6938defd4be --- /dev/null +++ b/packages/SystemUI/res/anim/long_press_lock_screen_popup_exit.xml @@ -0,0 +1,39 @@ +<?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. + --> + +<set + xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + + <scale + android:interpolator="@android:anim/accelerate_interpolator" + android:duration="233" + android:fromXScale="1" + android:fromYScale="1" + android:toXScale="0.5" + android:toYScale="0.5" + android:pivotX="50%" + android:pivotY="50%" /> + + <alpha + android:interpolator="@android:anim/linear_interpolator" + android:delay="150" + android:duration="83" + android:fromAlpha="1" + android:toAlpha="0" /> + +</set> diff --git a/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml index 3807b92ae39d..a0ceb81d42f4 100644 --- a/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml +++ b/packages/SystemUI/res/drawable/keyguard_settings_popup_menu_background.xml @@ -17,17 +17,17 @@ <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"> + android:color="#4d000000"> <item android:id="@android:id/mask"> <shape android:shape="rectangle"> <solid android:color="@android:color/white"/> - <corners android:radius="28dp" /> + <corners android:radius="@dimen/keyguard_affordance_fixed_radius" /> </shape> </item> <item> <shape android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface" /> - <corners android:radius="28dp" /> + <solid android:color="?androidprv:attr/materialColorOnBackground" /> + <corners android:radius="@dimen/keyguard_affordance_fixed_radius" /> </shape> </item> </ripple> diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 4048a39344bd..c0f7029449a1 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -20,7 +20,7 @@ android:id="@+id/keyguard_bottom_area" android:layout_height="match_parent" android:layout_width="match_parent" - android:outlineProvider="none" > <!-- Put it above the status bar header --> + android:outlineProvider="none" > <LinearLayout android:id="@+id/keyguard_indication_area" @@ -59,33 +59,58 @@ </LinearLayout> - <com.android.systemui.animation.view.LaunchableImageView - android:id="@+id/start_button" - android:layout_height="@dimen/keyguard_affordance_fixed_height" - android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:layout_gravity="bottom|start" - android:scaleType="fitCenter" - android:padding="@dimen/keyguard_affordance_fixed_padding" - 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" /> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_gravity="bottom" + > + + <com.android.systemui.animation.view.LaunchableImageView + android:id="@+id/start_button" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_width="@dimen/keyguard_affordance_fixed_width" + android:layout_gravity="bottom|start" + android:scaleType="fitCenter" + android:padding="@dimen/keyguard_affordance_fixed_padding" + 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="invisible" /> - <com.android.systemui.animation.view.LaunchableImageView - android:id="@+id/end_button" - android:layout_height="@dimen/keyguard_affordance_fixed_height" - android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:layout_gravity="bottom|end" - android:scaleType="fitCenter" - android:padding="@dimen/keyguard_affordance_fixed_padding" - 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" /> + <FrameLayout + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:paddingHorizontal="24dp" + > + <include + android:id="@+id/keyguard_settings_button" + layout="@layout/keyguard_settings_popup_menu" + android:layout_width="wrap_content" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_gravity="center" + android:visibility="gone" + /> + </FrameLayout> + + <com.android.systemui.animation.view.LaunchableImageView + android:id="@+id/end_button" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_width="@dimen/keyguard_affordance_fixed_width" + android:layout_gravity="bottom|end" + android:scaleType="fitCenter" + android:padding="@dimen/keyguard_affordance_fixed_padding" + 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="invisible" /> + + </LinearLayout> <FrameLayout android:id="@+id/overlay_container" diff --git a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml index 89d88fea1817..9d0d783d0113 100644 --- a/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml +++ b/packages/SystemUI/res/layout/keyguard_settings_popup_menu.xml @@ -15,25 +15,23 @@ ~ --> -<LinearLayout +<com.android.systemui.animation.view.LaunchableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:minHeight="52dp" + android:layout_height="48dp" android:orientation="horizontal" android:gravity="center_vertical" android:background="@drawable/keyguard_settings_popup_menu_background" - android:paddingStart="16dp" - android:paddingEnd="24dp" - android:paddingVertical="16dp"> + android:padding="12dp"> <ImageView android:id="@+id/icon" - android:layout_width="20dp" - android:layout_height="20dp" - android:layout_marginEnd="16dp" - android:tint="?android:attr/textColorPrimary" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginEnd="8dp" + android:tint="?androidprv:attr/materialColorOnSecondaryFixed" android:importantForAccessibility="no" tools:ignore="UseAppTint" /> @@ -42,9 +40,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSecondaryFixed" android:textSize="14sp" android:maxLines="1" android:ellipsize="end" /> -</LinearLayout>
\ No newline at end of file +</com.android.systemui.animation.view.LaunchableLinearLayout> diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index a11ffcda9830..f1fca7603571 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -120,10 +120,6 @@ /> </com.android.systemui.shade.NotificationsQuickSettingsContainer> - <include - layout="@layout/keyguard_bottom_area" - android:visibility="gone" /> - <ViewStub android:id="@+id/keyguard_user_switcher_stub" android:layout="@layout/keyguard_user_switcher" @@ -153,6 +149,10 @@ </com.android.keyguard.LockIconView> + <include + layout="@layout/keyguard_bottom_area" + android:visibility="gone" /> + <FrameLayout android:id="@+id/preview_container" android:layout_width="match_parent" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 714d495af762..4db42aacbdcd 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1774,13 +1774,6 @@ <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> - - <!-- Bouncer user switcher margins --> <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f1777f84cd6d..74ae954a539c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3057,13 +3057,17 @@ <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. + Label for a menu item in a menu that is shown when the user wishes to customize the lock screen. Clicking on this menu item takes the user to a screen where they can modify the settings of the lock screen. + It is critical that this text is as short as possible. If needed, translators should feel free + to drop "lock screen" from their translation and keep just "Customize" or "Customization", in + cases when that verb is not available in their target language. + [CHAR LIMIT=32] --> - <string name="lock_screen_settings">Lock screen settings</string> + <string name="lock_screen_settings">Customize lock screen</string> <!-- Content description for Wi-Fi not available icon on dream [CHAR LIMIT=NONE]--> <string name="wifi_unavailable_dream_overlay_content_description">Wi-Fi not available</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 064cea112b5a..2098aea87ab2 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1399,4 +1399,9 @@ <style name="ShortcutItemBackground"> <item name="android:background">@color/ksh_key_item_new_background</item> </style> + + <style name="LongPressLockScreenAnimation"> + <item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item> + <item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item> + </style> </resources> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardActiveUnlockModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardActiveUnlockModel.kt index 3a89c13ddd64..40f6f48288dc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardActiveUnlockModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardActiveUnlockModel.kt @@ -17,9 +17,9 @@ package com.android.keyguard import android.annotation.CurrentTimeMillisLong +import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.plugins.util.RingBuffer /** Verbose debug information. */ data class KeyguardActiveUnlockModel( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index c98e9b40e7ab..5b0e29005d82 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -17,9 +17,9 @@ package com.android.keyguard import android.annotation.CurrentTimeMillisLong +import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.plugins.util.RingBuffer /** Verbose debug information associated. */ data class KeyguardFaceListenModel( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt index 57130ed80d26..b8c0ccbd8aaa 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt @@ -17,9 +17,9 @@ package com.android.keyguard import android.annotation.CurrentTimeMillisLong +import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.plugins.util.RingBuffer /** Verbose debug information. */ data class KeyguardFingerprintListenModel( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index c4df836e401f..6f549881d12a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -16,12 +16,37 @@ package com.android.keyguard; +import static androidx.constraintlayout.widget.ConstraintSet.END; +import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; + +import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; + +import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.Nullable; import android.graphics.Rect; +import android.transition.ChangeBounds; +import android.transition.Transition; +import android.transition.TransitionListenerAdapter; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.transition.TransitionValues; import android.util.Slog; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; + +import com.android.internal.jank.InteractionJankMonitor; import com.android.keyguard.KeyguardClockSwitch.ClockSize; import com.android.keyguard.logging.KeyguardLogger; +import com.android.systemui.R; +import com.android.systemui.animation.Interpolators; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ClockController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; @@ -42,6 +67,12 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardStatusViewController"; + /** + * Duration to use for the animator when the keyguard status view alignment changes, and a + * custom clock animation is in use. + */ + private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000; + private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); @@ -50,8 +81,25 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; private final KeyguardVisibilityHelper mKeyguardVisibilityHelper; + private final FeatureFlags mFeatureFlags; + private final InteractionJankMonitor mInteractionJankMonitor; private final Rect mClipBounds = new Rect(); + private Boolean mStatusViewCentered = true; + + private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener = + new TransitionListenerAdapter() { + @Override + public void onTransitionCancel(Transition transition) { + mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + } + + @Override + public void onTransitionEnd(Transition transition) { + mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + } + }; + @Inject public KeyguardStatusViewController( KeyguardStatusView keyguardStatusView, @@ -62,7 +110,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV ConfigurationController configurationController, DozeParameters dozeParameters, ScreenOffAnimationController screenOffAnimationController, - KeyguardLogger logger) { + KeyguardLogger logger, + FeatureFlags featureFlags, + InteractionJankMonitor interactionJankMonitor) { super(keyguardStatusView); mKeyguardSliceViewController = keyguardSliceViewController; mKeyguardClockSwitchController = keyguardClockSwitchController; @@ -71,6 +121,8 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController, dozeParameters, screenOffAnimationController, /* animateYPos= */ true, logger.getBuffer()); + mInteractionJankMonitor = interactionJankMonitor; + mFeatureFlags = featureFlags; } @Override @@ -242,9 +294,141 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } } - /** Gets the current clock controller. */ - @Nullable - public ClockController getClockController() { - return mKeyguardClockSwitchController.getClock(); + /** + * Updates the alignment of the KeyguardStatusView and animates the transition if requested. + */ + public void updateAlignment( + ConstraintLayout notifContainerParent, + boolean splitShadeEnabled, + boolean shouldBeCentered, + boolean animate) { + if (mStatusViewCentered == shouldBeCentered) { + return; + } + + mStatusViewCentered = shouldBeCentered; + if (notifContainerParent == null) { + return; + } + + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(notifContainerParent); + int statusConstraint = shouldBeCentered ? PARENT_ID : R.id.qs_edge_guideline; + constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); + if (!animate) { + constraintSet.applyTo(notifContainerParent); + return; + } + + mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); + ChangeBounds transition = new ChangeBounds(); + if (splitShadeEnabled) { + // Excluding media from the transition on split-shade, as it doesn't transition + // horizontally properly. + transition.excludeTarget(R.id.status_view_media_container, true); + } + + transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + + ClockController clock = mKeyguardClockSwitchController.getClock(); + boolean customClockAnimation = clock != null + && clock.getConfig().getHasCustomPositionUpdatedAnimation(); + + if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { + // Find the clock, so we can exclude it from this transition. + FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large); + + // The clock container can sometimes be null. If it is, just fall back to the + // old animation rather than setting up the custom animations. + if (clockContainerView == null || clockContainerView.getChildCount() == 0) { + transition.addListener(mKeyguardStatusAlignmentTransitionListener); + TransitionManager.beginDelayedTransition(notifContainerParent, transition); + } else { + View clockView = clockContainerView.getChildAt(0); + + transition.excludeTarget(clockView, /* exclude= */ true); + + TransitionSet set = new TransitionSet(); + set.addTransition(transition); + + SplitShadeTransitionAdapter adapter = + new SplitShadeTransitionAdapter(mKeyguardClockSwitchController); + + // Use linear here, so the actual clock can pick its own interpolator. + adapter.setInterpolator(Interpolators.LINEAR); + adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION); + adapter.addTarget(clockView); + set.addTransition(adapter); + set.addListener(mKeyguardStatusAlignmentTransitionListener); + TransitionManager.beginDelayedTransition(notifContainerParent, set); + } + } else { + transition.addListener(mKeyguardStatusAlignmentTransitionListener); + TransitionManager.beginDelayedTransition(notifContainerParent, transition); + } + + constraintSet.applyTo(notifContainerParent); + } + + @VisibleForTesting + static class SplitShadeTransitionAdapter extends Transition { + private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; + private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; + + private final KeyguardClockSwitchController mController; + + @VisibleForTesting + SplitShadeTransitionAdapter(KeyguardClockSwitchController controller) { + mController = controller; + } + + private void captureValues(TransitionValues transitionValues) { + Rect boundsRect = new Rect(); + boundsRect.left = transitionValues.view.getLeft(); + boundsRect.top = transitionValues.view.getTop(); + boundsRect.right = transitionValues.view.getRight(); + boundsRect.bottom = transitionValues.view.getBottom(); + transitionValues.values.put(PROP_BOUNDS, boundsRect); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Nullable + @Override + public Animator createAnimator(ViewGroup sceneRoot, @Nullable TransitionValues startValues, + @Nullable TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + + Rect from = (Rect) startValues.values.get(PROP_BOUNDS); + Rect to = (Rect) endValues.values.get(PROP_BOUNDS); + + anim.addUpdateListener(animation -> { + ClockController clock = mController.getClock(); + if (clock == null) { + return; + } + + clock.getAnimations().onPositionUpdated(from, to, animation.getAnimatedFraction()); + }); + + return anim; + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index e1bca89091b2..350c4ed084b9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -96,6 +96,7 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.SensorProperties; +import android.hardware.biometrics.SensorPropertiesInternal; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; @@ -1278,6 +1279,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) { lockedOutStateChanged = !mFaceLockedOutPermanent; mFaceLockedOutPermanent = true; + if (isFaceClass3()) { + updateFingerprintListeningState(BIOMETRIC_ACTION_STOP); + } } if (isHwUnavailable && cameraPrivacyEnabled) { @@ -1487,8 +1491,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // STRONG_AUTH_REQUIRED_AFTER_LOCKOUT which is the same as mFingerprintLockedOutPermanent; // however the strong auth tracker does not include the temporary lockout // mFingerprintLockedOut. + // Class 3 biometric lockout will lockout ALL biometrics return mStrongAuthTracker.isUnlockingWithBiometricAllowed(isStrongBiometric) - && !mFingerprintLockedOut; + && (!isFingerprintClass3() || !isFingerprintLockedOut()) + && (!isFaceClass3() || !mFaceLockedOutPermanent); } /** @@ -1506,9 +1512,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @NonNull BiometricSourceType biometricSourceType) { switch (biometricSourceType) { case FINGERPRINT: - return isUnlockingWithBiometricAllowed(true); + return isUnlockingWithBiometricAllowed(isFingerprintClass3()); case FACE: - return isUnlockingWithBiometricAllowed(false); + return isUnlockingWithBiometricAllowed(isFaceClass3()); default: return false; } @@ -2473,7 +2479,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void updateFaceEnrolled(int userId) { - Boolean isFaceEnrolled = mFaceManager != null && !mFaceSensorProperties.isEmpty() + final Boolean isFaceEnrolled = isFaceSupported() && mBiometricEnabledForUser.get(userId) && mAuthController.isFaceAuthEnrolled(userId); if (mIsFaceEnrolled != isFaceEnrolled) { @@ -2482,10 +2488,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mIsFaceEnrolled = isFaceEnrolled; } - public boolean isFaceSupported() { + private boolean isFaceSupported() { return mFaceManager != null && !mFaceSensorProperties.isEmpty(); } + private boolean isFingerprintSupported() { + return mFpm != null && !mFingerprintSensorProperties.isEmpty(); + } + /** * @return true if there's at least one udfps enrolled for the current user. */ @@ -2792,10 +2802,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || !mLockPatternUtils.isSecure(user); // Don't trigger active unlock if fp is locked out - final boolean fpLockedOut = mFingerprintLockedOut || mFingerprintLockedOutPermanent; + final boolean fpLockedOut = isFingerprintLockedOut(); // Don't trigger active unlock if primary auth is required - final boolean primaryAuthRequired = !isUnlockingWithBiometricAllowed(true); + final boolean primaryAuthRequired = !isUnlockingWithTrustAgentAllowed(); final boolean shouldTriggerActiveUnlock = (mAuthInterruptActive || triggerActiveUnlockForAssistant || awakeKeyguard) @@ -2857,7 +2867,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || mGoingToSleep || shouldListenForFingerprintAssistant || (mKeyguardOccluded && mIsDreaming) - || (mKeyguardOccluded && userDoesNotHaveTrust + || (mKeyguardOccluded && userDoesNotHaveTrust && mKeyguardShowing && (mOccludingAppRequestingFp || isUdfps || mAlternateBouncerShowing)); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an @@ -2949,7 +2959,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // allow face detection to happen even if stronger auth is required. When face is detected, // we show the bouncer. However, if the user manually locked down the device themselves, // never attempt to detect face. - final boolean supportsDetect = !mFaceSensorProperties.isEmpty() + final boolean supportsDetect = isFaceSupported() && mFaceSensorProperties.get(0).supportsFaceDetection && canBypass && !mPrimaryBouncerIsOrWillBeShowing && !isUserInLockdown(user); @@ -3104,7 +3114,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab : WAKE_REASON_UNKNOWN ).toFaceAuthenticateOptions(); // This would need to be updated for multi-sensor devices - final boolean supportsFaceDetection = !mFaceSensorProperties.isEmpty() + final boolean supportsFaceDetection = isFaceSupported() && mFaceSensorProperties.get(0).supportsFaceDetection; if (!isUnlockingWithBiometricAllowed(FACE)) { final boolean udfpsFingerprintAuthRunning = isUdfpsSupported() @@ -3166,21 +3176,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @return {@code true} if possible. */ public boolean isUnlockingWithNonStrongBiometricsPossible(int userId) { - // This assumes that there is at most one face and at most one fingerprint sensor - return (mFaceManager != null && !mFaceSensorProperties.isEmpty() - && (mFaceSensorProperties.get(0).sensorStrength != SensorProperties.STRENGTH_STRONG) - && isUnlockWithFacePossible(userId)) - || (mFpm != null && !mFingerprintSensorProperties.isEmpty() - && (mFingerprintSensorProperties.get(0).sensorStrength - != SensorProperties.STRENGTH_STRONG) && isUnlockWithFingerprintPossible(userId)); + return (!isFaceClass3() && isUnlockWithFacePossible(userId)) + || (isFingerprintClass3() && isUnlockWithFingerprintPossible(userId)); } @SuppressLint("MissingPermission") @VisibleForTesting boolean isUnlockWithFingerprintPossible(int userId) { // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once. - boolean newFpEnrolled = mFpm != null - && !mFingerprintSensorProperties.isEmpty() + boolean newFpEnrolled = isFingerprintSupported() && !isFingerprintDisabled(userId) && mFpm.hasEnrolledTemplates(userId); Boolean oldFpEnrolled = mIsUnlockWithFingerprintPossible.getOrDefault(userId, false); if (oldFpEnrolled != newFpEnrolled) { @@ -3330,12 +3334,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Immediately stop previous biometric listening states. // Resetting lockout states updates the biometric listening states. - if (mFaceManager != null && !mFaceSensorProperties.isEmpty()) { + if (isFaceSupported()) { stopListeningForFace(FACE_AUTH_UPDATED_USER_SWITCHING); handleFaceLockoutReset(mFaceManager.getLockoutModeForUser( mFaceSensorProperties.get(0).sensorId, userId)); } - if (mFpm != null && !mFingerprintSensorProperties.isEmpty()) { + if (isFingerprintSupported()) { stopListeningForFingerprint(); handleFingerprintLockoutReset(mFpm.getLockoutModeForUser( mFingerprintSensorProperties.get(0).sensorId, userId)); @@ -4071,6 +4075,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return BIOMETRIC_LOCKOUT_RESET_DELAY_MS; } + @VisibleForTesting + protected boolean isFingerprintClass3() { + // This assumes that there is at most one fingerprint sensor property + return isFingerprintSupported() && isClass3Biometric(mFingerprintSensorProperties.get(0)); + } + + @VisibleForTesting + protected boolean isFaceClass3() { + // This assumes that there is at most one face sensor property + return isFaceSupported() && isClass3Biometric(mFaceSensorProperties.get(0)); + } + + private boolean isClass3Biometric(SensorPropertiesInternal sensorProperties) { + return sensorProperties.sensorStrength == SensorProperties.STRENGTH_STRONG; + } + /** * Unregister all listeners. */ @@ -4122,11 +4142,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab for (int subId : mServiceStates.keySet()) { pw.println(" " + subId + "=" + mServiceStates.get(subId)); } - if (mFpm != null && !mFingerprintSensorProperties.isEmpty()) { + if (isFingerprintSupported()) { final int userId = mUserTracker.getUserId(); final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId); pw.println(" Fingerprint state (user=" + userId + ")"); + pw.println(" isFingerprintClass3=" + isFingerprintClass3()); pw.println(" areAllFpAuthenticatorsRegistered=" + mAuthController.areAllFingerprintAuthenticatorsRegistered()); pw.println(" allowed=" @@ -4184,11 +4205,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintListenBuffer.toList() ).printTableData(pw); } - if (mFaceManager != null && !mFaceSensorProperties.isEmpty()) { + if (isFaceSupported()) { final int userId = mUserTracker.getUserId(); final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); BiometricAuthenticated face = mUserFaceAuthenticated.get(userId); pw.println(" Face authentication state (user=" + userId + ")"); + pw.println(" isFaceClass3=" + isFaceClass3()); pw.println(" allowed=" + (face != null && isUnlockingWithBiometricAllowed(face.mIsStrongBiometric))); pw.println(" auth'd=" diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java index ac0a3fd8dbc4..a678edc0eb06 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java @@ -28,7 +28,6 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; -import com.android.systemui.statusbar.phone.AnimatorHandle; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -48,7 +47,6 @@ public class KeyguardVisibilityHelper { private final ScreenOffAnimationController mScreenOffAnimationController; private boolean mAnimateYPos; private boolean mKeyguardViewVisibilityAnimating; - private AnimatorHandle mKeyguardAnimatorHandle; private boolean mLastOccludedState = false; private final AnimationProperties mAnimationProperties = new AnimationProperties(); private final LogBuffer mLogBuffer; @@ -85,10 +83,6 @@ public class KeyguardVisibilityHelper { boolean keyguardFadingAway, boolean goingToFullShade, int oldStatusBarState) { - if (mKeyguardAnimatorHandle != null) { - mKeyguardAnimatorHandle.cancel(); - mKeyguardAnimatorHandle = null; - } mView.animate().cancel(); boolean isOccluded = mKeyguardStateController.isOccluded(); mKeyguardViewVisibilityAnimating = false; @@ -122,7 +116,7 @@ public class KeyguardVisibilityHelper { .setDuration(320) .setInterpolator(Interpolators.ALPHA_IN) .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable); - log("keyguardFadingAway transition w/ Y Animation"); + log("keyguardFadingAway transition w/ Y Aniamtion"); } else if (statusBarState == KEYGUARD) { if (keyguardFadingAway) { mKeyguardViewVisibilityAnimating = true; @@ -154,7 +148,7 @@ public class KeyguardVisibilityHelper { // Ask the screen off animation controller to animate the keyguard visibility for us // since it may need to be cancelled due to keyguard lifecycle events. - mKeyguardAnimatorHandle = mScreenOffAnimationController.animateInKeyguard( + mScreenOffAnimationController.animateInKeyguard( mView, mAnimateKeyguardStatusViewVisibleEndRunnable); } else { log("Direct set Visibility to VISIBLE"); diff --git a/packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt b/packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt deleted file mode 100644 index 2f03259766c0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ChooserPinMigration.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui - -import android.content.ComponentName -import android.content.Context -import android.content.Context.MODE_PRIVATE -import android.content.Intent -import android.content.SharedPreferences -import android.os.Bundle -import android.os.Environment -import android.os.storage.StorageManager -import android.util.Log -import androidx.core.util.Supplier -import com.android.internal.R -import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import java.io.File -import javax.inject.Inject - -/** - * Performs a migration of pinned targets to the unbundled chooser if legacy data exists. - * - * Sends an explicit broadcast with the contents of the legacy pin preferences. The broadcast is - * protected by the RECEIVE_CHOOSER_PIN_MIGRATION permission. This class requires the - * ADD_CHOOSER_PINS permission in order to be able to send this broadcast. - */ -class ChooserPinMigration -@Inject -constructor( - private val context: Context, - private val featureFlags: FeatureFlags, - private val broadcastSender: BroadcastSender, - legacyPinPrefsFileSupplier: LegacyPinPrefsFileSupplier, -) : CoreStartable { - - private val legacyPinPrefsFile = legacyPinPrefsFileSupplier.get() - private val chooserComponent = - ComponentName.unflattenFromString( - context.resources.getString(R.string.config_chooserActivity) - ) - - override fun start() { - if (migrationIsRequired()) { - doMigration() - } - } - - private fun migrationIsRequired(): Boolean { - return featureFlags.isEnabled(Flags.CHOOSER_MIGRATION_ENABLED) && - legacyPinPrefsFile.exists() && - chooserComponent?.packageName != null - } - - private fun doMigration() { - Log.i(TAG, "Beginning migration") - - val legacyPinPrefs = context.getSharedPreferences(legacyPinPrefsFile, MODE_PRIVATE) - - if (legacyPinPrefs.all.isEmpty()) { - Log.i(TAG, "No data to migrate, deleting legacy file") - } else { - sendSharedPreferences(legacyPinPrefs) - Log.i(TAG, "Legacy data sent, deleting legacy preferences") - - val legacyPinPrefsEditor = legacyPinPrefs.edit() - legacyPinPrefsEditor.clear() - if (!legacyPinPrefsEditor.commit()) { - Log.e(TAG, "Failed to delete legacy preferences") - return - } - } - - if (!legacyPinPrefsFile.delete()) { - Log.e(TAG, "Legacy preferences deleted, but failed to delete legacy preferences file") - return - } - - Log.i(TAG, "Legacy preference deletion complete") - } - - private fun sendSharedPreferences(sharedPreferences: SharedPreferences) { - val bundle = Bundle() - - sharedPreferences.all.entries.forEach { (key, value) -> - when (value) { - is Boolean -> bundle.putBoolean(key, value) - else -> Log.e(TAG, "Unsupported preference type for $key: ${value?.javaClass}") - } - } - - sendBundle(bundle) - } - - private fun sendBundle(bundle: Bundle) { - val intent = - Intent().apply { - `package` = chooserComponent?.packageName!! - action = BROADCAST_ACTION - putExtras(bundle) - } - broadcastSender.sendBroadcast(intent, BROADCAST_PERMISSION) - } - - companion object { - private const val TAG = "PinnedShareTargetMigration" - private const val BROADCAST_ACTION = "android.intent.action.CHOOSER_PIN_MIGRATION" - private const val BROADCAST_PERMISSION = "android.permission.RECEIVE_CHOOSER_PIN_MIGRATION" - - class LegacyPinPrefsFileSupplier @Inject constructor(private val context: Context) : - Supplier<File> { - - override fun get(): File { - val packageDirectory = - Environment.getDataUserCePackageDirectory( - StorageManager.UUID_PRIVATE_INTERNAL, - context.userId, - context.packageName, - ) - val sharedPrefsDirectory = File(packageDirectory, "shared_prefs") - return File(sharedPrefsDirectory, "chooser_pin_settings.xml") - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index ac30311e1f96..aabdafb88558 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -563,6 +563,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { (TouchProcessorResult.ProcessedTouch) result; final NormalizedTouchData data = processedTouch.getTouchData(); + boolean shouldPilfer = false; mActivePointerId = processedTouch.getPointerOnSensorId(); switch (processedTouch.getEvent()) { case DOWN: @@ -581,8 +582,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mStatusBarStateController.isDozing()); // Pilfer if valid overlap, don't allow following events to reach keyguard - mInputManager.pilferPointers( - mOverlay.getOverlayView().getViewRootImpl().getInputToken()); + shouldPilfer = true; break; case UP: @@ -621,6 +621,12 @@ public class UdfpsController implements DozeReceiver, Dumpable { // Always pilfer pointers that are within sensor area or when alternate bouncer is showing if (isWithinSensorArea(mOverlay.getOverlayView(), event.getRawX(), event.getRawY(), true) || mAlternateBouncerInteractor.isVisibleState()) { + shouldPilfer = true; + } + + // Execute the pilfer, never pilfer if a vertical swipe is in progress + if (shouldPilfer && mLockscreenShadeTransitionController.getQSDragProgress() == 0f + && !mPrimaryBouncerInteractor.isInTransit()) { mInputManager.pilferPointers( mOverlay.getOverlayView().getViewRootImpl().getInputToken()); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index f6b71336675f..691017b220f8 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -324,10 +324,6 @@ public class BrightLineFalsingManager implements FalsingManager { @Override public boolean isFalseLongTap(@Penalty int penalty) { - if (!mFeatureFlags.isEnabled(Flags.FALSING_FOR_LONG_TAPS)) { - return false; - } - checkDestroyed(); if (skipFalsing(GENERIC)) { diff --git a/keystore/java/android/security/GenerateRkpKeyException.java b/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt index a2d65e4e7119..26fc36dd3c9e 100644 --- a/keystore/java/android/security/GenerateRkpKeyException.java +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/MotionEventExt.kt @@ -1,11 +1,11 @@ /* - * Copyright (C) 2021 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,18 +14,15 @@ * limitations under the License. */ -package android.security; +package com.android.systemui.common.ui.view -/** - * Thrown on problems in attempting to attest to a key using a remotely provisioned key. - * - * @hide - */ -public class GenerateRkpKeyException extends Exception { +import android.util.MathUtils +import android.view.MotionEvent - /** - * Constructs a new {@code GenerateRkpKeyException}. - */ - public GenerateRkpKeyException() { - } +/** Returns the distance from the position of this [MotionEvent] and the given coordinates. */ +fun MotionEvent.distanceFrom( + x: Float, + y: Float, +): Float { + return MathUtils.dist(this.x, this.y, x, y) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index df236e7f9c8f..9bf6b2a5b42b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -17,7 +17,6 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger -import com.android.systemui.ChooserPinMigration import com.android.systemui.ChooserSelector import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester @@ -76,13 +75,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(AuthController::class) abstract fun bindAuthController(service: AuthController): CoreStartable - /** Inject into ChooserPinMigration. */ - @Binds - @IntoMap - @ClassKey(ChooserPinMigration::class) - @PerUser - abstract fun bindChooserPinMigration(sysui: ChooserPinMigration): CoreStartable - /** Inject into ChooserCoreStartable. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 63a4fd2189d8..7945470b424a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -85,6 +85,8 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; import com.android.systemui.statusbar.notification.people.PeopleHubModule; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; @@ -116,16 +118,16 @@ import com.android.systemui.wallet.dagger.WalletModule; import com.android.systemui.wmshell.BubblesManager; import com.android.wm.shell.bubbles.Bubbles; -import java.util.Optional; -import java.util.concurrent.Executor; - -import javax.inject.Named; - import dagger.Binds; import dagger.BindsOptionalOf; import dagger.Module; import dagger.Provides; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.inject.Named; + /** * A dagger module for injecting components of System UI that are required by System UI. * @@ -315,4 +317,11 @@ public abstract class SystemUIModule { @Binds abstract LargeScreenShadeInterpolator largeScreensShadeInterpolator( LargeScreenShadeInterpolatorImpl impl); + + @SysUISingleton + @Provides + static VisualInterruptionDecisionProvider provideVisualInterruptionDecisionProvider( + NotificationInterruptStateProvider innerProvider) { + return new NotificationInterruptStateProviderWrapper(innerProvider); + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 0913cfcd5d6a..45e4bb638621 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -103,6 +103,11 @@ object Flags { val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD = releasedFlag(254647461, "filter_unseen_notifs_on_keyguard") + // TODO(b/277338665): Tracking Bug + @JvmField + val NOTIFICATION_SHELF_REFACTOR = + unreleasedFlag(271161129, "notification_shelf_refactor") + // TODO(b/263414400): Tracking Bug @JvmField val NOTIFICATION_ANIMATE_BIG_PICTURE = @@ -132,6 +137,11 @@ object Flags { // TODO(b/254512676): Tracking Bug @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = releasedFlag(207, "lockscreen_custom_clocks") + // TODO(b/275694445): Tracking Bug + @JvmField + val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = unreleasedFlag(208, + "lockscreen_without_secure_lock_when_dreaming") + /** * Whether the clock on a wide lock screen should use the new "stepping" animation for moving * the digits when the clock moves. @@ -230,6 +240,12 @@ object Flags { @JvmField val REFACTOR_KEYGUARD_DISMISS_INTENT = unreleasedFlag(231, "refactor_keyguard_dismiss_intent") + /** Whether to allow long-press on the lock screen to directly open wallpaper picker. */ + // TODO(b/277220285): Tracking bug. + @JvmField + val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP = + unreleasedFlag(232, "lock_screen_long_press_directly_opens_wallpaper_picker") + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -395,7 +411,7 @@ object Flags { val MEDIA_RETAIN_RECOMMENDATIONS = releasedFlag(916, "media_retain_recommendations") // TODO(b/270437894): Tracking Bug - val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume") + val MEDIA_REMOTE_RESUME = unreleasedFlag(917, "media_remote_resume", teamfood = true) // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") @@ -605,9 +621,6 @@ object Flags { val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag(1504, "sharesheet_scrollable_image_preview") - // TODO(b/274137694) Tracking Bug - val CHOOSER_MIGRATION_ENABLED = unreleasedFlag(1505, "chooser_migration_enabled") - // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") @@ -637,9 +650,6 @@ object Flags { val APP_PANELS_REMOVE_APPS_ALLOWED = unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = true) - // 2100 - Falsing Manager - @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps") - // 2200 - udfps // TODO(b/259264861): Tracking Bug @JvmField val UDFPS_NEW_TOUCH_DETECTION = releasedFlag(2200, "udfps_new_touch_detection") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index c102c5b57375..416b2379eef8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -128,6 +128,8 @@ import com.android.systemui.classifier.FalsingCollector; 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; @@ -1184,6 +1186,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; private Lazy<ScrimController> mScrimControllerLazy; + private FeatureFlags mFeatureFlags; + /** * Injected constructor. See {@link KeyguardModule}. */ @@ -1214,7 +1218,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Lazy<ShadeController> shadeControllerLazy, Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy, Lazy<ActivityLaunchAnimator> activityLaunchAnimator, - Lazy<ScrimController> scrimControllerLazy) { + Lazy<ScrimController> scrimControllerLazy, + FeatureFlags featureFlags) { mContext = context; mUserTracker = userTracker; mFalsingCollector = falsingCollector; @@ -1269,6 +1274,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDreamOpenAnimationDuration = (int) DREAMING_ANIMATION_DURATION_MS; mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS; + + mFeatureFlags = featureFlags; } public void userActivity() { @@ -1682,14 +1689,17 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } /** - * A dream started. We should lock after the usual screen-off lock timeout but only - * if there is a secure lock pattern. + * A dream started. We should lock after the usual screen-off lock timeout regardless if + * there is a secure lock pattern or not */ public void onDreamingStarted() { mUpdateMonitor.dispatchDreamingStarted(); synchronized (this) { + final boolean alwaysShowKeyguard = + mFeatureFlags.isEnabled(Flags.LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING); if (mDeviceInteractive - && mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())) { + && (alwaysShowKeyguard || + mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()))) { doKeyguardLaterLocked(); } } 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 6ac51cd52b49..5e71458c8bb4 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.SysUISingleton; 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; @@ -119,7 +120,8 @@ public class KeyguardModule { Lazy<ShadeController> shadeController, Lazy<NotificationShadeWindowController> notificationShadeWindowController, Lazy<ActivityLaunchAnimator> activityLaunchAnimator, - Lazy<ScrimController> scrimControllerLazy) { + Lazy<ScrimController> scrimControllerLazy, + FeatureFlags featureFlags) { return new KeyguardViewMediator( context, userTracker, @@ -149,7 +151,8 @@ public class KeyguardModule { shadeController, notificationShadeWindowController, activityLaunchAnimator, - scrimControllerLazy); + scrimControllerLazy, + featureFlags); } /** */ 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 8ece3183fd56..ab4abbf3d575 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 @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository import android.content.Context import android.os.UserHandle +import android.util.LayoutDirection import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging @@ -113,30 +114,6 @@ constructor( initialValue = emptyMap(), ) - private val _slotPickerRepresentations: List<KeyguardSlotPickerRepresentation> by lazy { - fun parseSlot(unparsedSlot: String): Pair<String, Int> { - val split = unparsedSlot.split(SLOT_CONFIG_DELIMITER) - check(split.size == 2) - val slotId = split[0] - val slotCapacity = split[1].toInt() - return slotId to slotCapacity - } - - val unparsedSlots = - appContext.resources.getStringArray(R.array.config_keyguardQuickAffordanceSlots) - - val seenSlotIds = mutableSetOf<String>() - unparsedSlots.mapNotNull { unparsedSlot -> - val (slotId, slotCapacity) = parseSlot(unparsedSlot) - check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" } - seenSlotIds.add(slotId) - KeyguardSlotPickerRepresentation( - id = slotId, - maxSelectedAffordances = slotCapacity, - ) - } - } - init { legacySettingSyncer.startSyncing() dumpManager.registerDumpable("KeyguardQuickAffordances", Dumpster()) @@ -211,7 +188,30 @@ constructor( * each slot and select which affordance(s) is/are installed in each slot on the keyguard. */ fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { - return _slotPickerRepresentations + fun parseSlot(unparsedSlot: String): Pair<String, Int> { + val split = unparsedSlot.split(SLOT_CONFIG_DELIMITER) + check(split.size == 2) + val slotId = split[0] + val slotCapacity = split[1].toInt() + return slotId to slotCapacity + } + + val unparsedSlots = + appContext.resources.getStringArray(R.array.config_keyguardQuickAffordanceSlots) + if (appContext.resources.configuration.layoutDirection == LayoutDirection.RTL) { + unparsedSlots.reverse() + } + + val seenSlotIds = mutableSetOf<String>() + return unparsedSlots.mapNotNull { unparsedSlot -> + val (slotId, slotCapacity) = parseSlot(unparsedSlot) + check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" } + seenSlotIds.add(slotId) + KeyguardSlotPickerRepresentation( + id = slotId, + maxSelectedAffordances = slotCapacity, + ) + } } private inner class Dumpster : Dumpable { 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 index 6525a13fc44f..ea6700e92731 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt @@ -17,29 +17,29 @@ package com.android.systemui.keyguard.domain.interactor -import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.view.accessibility.AccessibilityManager +import androidx.annotation.VisibleForTesting 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 com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay 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.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** Business logic for use-cases related to the keyguard long-press feature. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -54,18 +55,16 @@ import kotlinx.coroutines.flow.stateIn class KeyguardLongPressInteractor @Inject constructor( - @Application unsafeContext: Context, - @Application scope: CoroutineScope, + @Application private val scope: CoroutineScope, transitionInteractor: KeyguardTransitionInteractor, repository: KeyguardRepository, - private val activityStarter: ActivityStarter, private val logger: UiEventLogger, private val featureFlags: FeatureFlags, broadcastDispatcher: BroadcastDispatcher, + private val accessibilityManager: AccessibilityManagerWrapper, ) { - private val appContext = unsafeContext.applicationContext - - private val _isLongPressHandlingEnabled: StateFlow<Boolean> = + /** Whether the long-press handling feature should be enabled. */ + val isLongPressHandlingEnabled: StateFlow<Boolean> = if (isFeatureEnabled()) { combine( transitionInteractor.finishedKeyguardState.map { @@ -84,19 +83,35 @@ constructor( 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) + private val _isMenuVisible = MutableStateFlow(false) + /** Model for whether the menu should be shown. */ + val isMenuVisible: StateFlow<Boolean> = + isLongPressHandlingEnabled + .flatMapLatest { isEnabled -> + if (isEnabled) { + _isMenuVisible.asStateFlow() + } else { + // Reset the state so we don't see a menu when long-press handling is enabled + // again in the future. + _isMenuVisible.value = false + flowOf(false) + } } - } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + + private val _shouldOpenSettings = MutableStateFlow(false) + /** + * Whether the long-press accessible "settings" flow should be opened. + * + * Note that [onSettingsShown] must be invoked to consume this, once the settings are opened. + */ + val shouldOpenSettings = _shouldOpenSettings.asStateFlow() + + private var delayedHideMenuJob: Job? = null init { if (isFeatureEnabled()) { @@ -110,15 +125,46 @@ constructor( } /** Notifies that the user has long-pressed on the lock screen. */ - fun onLongPress(x: Int, y: Int) { - if (!_isLongPressHandlingEnabled.value) { + fun onLongPress() { + if (!isLongPressHandlingEnabled.value) { return } - showMenu( - x = x, - y = y, - ) + if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) { + showSettings() + } else { + showMenu() + } + } + + /** Notifies that the user has touched outside of the pop-up. */ + fun onTouchedOutside() { + hideMenu() + } + + /** Notifies that the user has started a touch gesture on the menu. */ + fun onMenuTouchGestureStarted() { + cancelAutomaticMenuHiding() + } + + /** Notifies that the user has started a touch gesture on the menu. */ + fun onMenuTouchGestureEnded(isClick: Boolean) { + if (isClick) { + hideMenu() + logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED) + showSettings() + } else { + scheduleAutomaticMenuHiding() + } + } + + /** Notifies that the settings UI has been shown, consuming the event to show it. */ + fun onSettingsShown() { + _shouldOpenSettings.value = false + } + + private fun showSettings() { + _shouldOpenSettings.value = true } private fun isFeatureEnabled(): Boolean { @@ -126,51 +172,40 @@ constructor( 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() }, - ) + /** Updates application state to ask to show the menu. */ + private fun showMenu() { + _isMenuVisible.value = true + scheduleAutomaticMenuHiding() logger.log(LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN) } + private fun scheduleAutomaticMenuHiding() { + cancelAutomaticMenuHiding() + delayedHideMenuJob = + scope.launch { + delay(timeOutMs()) + hideMenu() + } + } + /** Updates application state to ask to hide the menu. */ private fun hideMenu() { - _menu.value = null + cancelAutomaticMenuHiding() + _isMenuVisible.value = false } - /** 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, - ) + private fun cancelAutomaticMenuHiding() { + delayedHideMenuJob?.cancel() + delayedHideMenuJob = null + } + + private fun timeOutMs(): Long { + return accessibilityManager + .getRecommendedTimeoutMillis( + DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS.toInt(), + AccessibilityManager.FLAG_CONTENT_ICONS or AccessibilityManager.FLAG_CONTENT_TEXT, + ) + .toLong() } enum class LogEvents( @@ -184,4 +219,8 @@ constructor( override fun getId() = _id } + + companion object { + @VisibleForTesting const val DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS = 5000L + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt new file mode 100644 index 000000000000..568db2f543b6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.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.keyguard.ui.binder + +import android.os.VibrationEffect +import kotlin.time.Duration.Companion.milliseconds + +object KeyguardBottomAreaVibrations { + + val ShakeAnimationDuration = 300.milliseconds + const val ShakeAnimationCycles = 5f + + private const val SmallVibrationScale = 0.3f + private const val BigVibrationScale = 0.6f + + val Shake = + VibrationEffect.startComposition() + .apply { + val vibrationDelayMs = + (ShakeAnimationDuration.inWholeMilliseconds / ShakeAnimationCycles * 2).toInt() + val vibrationCount = ShakeAnimationCycles.toInt() * 2 + repeat(vibrationCount) { + addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + SmallVibrationScale, + vibrationDelayMs, + ) + } + } + .compose() + + val Activated = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + BigVibrationScale, + 0, + ) + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, + 0.1f, + 0, + ) + .compose() + + val Deactivated = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + BigVibrationScale, + 0, + ) + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, + 0.1f, + 0, + ) + .compose() +} 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 d63636c6fccc..68ac7e1c71e5 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 @@ -17,41 +17,42 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Rect import android.graphics.drawable.Animatable2 -import android.os.VibrationEffect import android.util.Size import android.util.TypedValue -import android.view.MotionEvent import android.view.View -import android.view.ViewConfiguration import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.widget.ImageView import android.widget.TextView -import androidx.core.animation.CycleInterpolator -import androidx.core.animation.ObjectAnimator +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.settingslib.Utils import com.android.systemui.R +import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.animation.Interpolators +import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper -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.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -91,15 +92,20 @@ object KeyguardBottomAreaViewBinder { * icon */ fun shouldConstrainToTopOfLockIcon(): Boolean + + /** Destroys this binding, releases resources, and cancels any coroutines. */ + fun destroy() } /** Binds the view to the view-model, continuing to update the former based on the latter. */ + @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( view: ViewGroup, viewModel: KeyguardBottomAreaViewModel, falsingManager: FalsingManager?, vibratorHelper: VibratorHelper?, + activityStarter: ActivityStarter?, messageDisplayer: (Int) -> Unit, ): Binding { val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area) @@ -110,137 +116,192 @@ object KeyguardBottomAreaViewBinder { val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text) val indicationTextBottom: TextView = view.requireViewById(R.id.keyguard_indication_text_bottom) + val settingsMenu: LaunchableLinearLayout = + view.requireViewById(R.id.keyguard_settings_button) view.clipChildren = false view.clipToPadding = false + view.setOnTouchListener { _, event -> + if (settingsMenu.isVisible) { + val hitRect = Rect() + settingsMenu.getHitRect(hitRect) + if (!hitRect.contains(event.x.toInt(), event.y.toInt())) { + viewModel.onTouchedOutsideLockScreenSettingsMenu() + } + } + + false + } val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.startButton.collect { buttonModel -> - updateButton( + val disposableHandle = + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.startButton.collect { buttonModel -> + updateButton( + view = startButton, + viewModel = buttonModel, + falsingManager = falsingManager, + messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, + ) + } + } + + launch { + viewModel.endButton.collect { buttonModel -> + updateButton( + view = endButton, + viewModel = buttonModel, + falsingManager = falsingManager, + messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, + ) + } + } + + launch { + viewModel.isOverlayContainerVisible.collect { isVisible -> + overlayContainer.visibility = + if (isVisible) { + View.VISIBLE + } else { + View.INVISIBLE + } + } + } + + launch { + viewModel.alpha.collect { alpha -> + view.importantForAccessibility = + if (alpha == 0f) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + + ambientIndicationArea?.alpha = alpha + indicationArea.alpha = alpha + } + } + + launch { + updateButtonAlpha( view = startButton, - viewModel = buttonModel, - falsingManager = falsingManager, - messageDisplayer = messageDisplayer, - vibratorHelper = vibratorHelper, + viewModel = viewModel.startButton, + alphaFlow = viewModel.alpha, ) } - } - launch { - viewModel.endButton.collect { buttonModel -> - updateButton( + launch { + updateButtonAlpha( view = endButton, - viewModel = buttonModel, - falsingManager = falsingManager, - messageDisplayer = messageDisplayer, - vibratorHelper = vibratorHelper, + viewModel = viewModel.endButton, + alphaFlow = viewModel.alpha, ) } - } - launch { - viewModel.isOverlayContainerVisible.collect { isVisible -> - overlayContainer.visibility = - if (isVisible) { - View.VISIBLE - } else { - View.INVISIBLE - } + launch { + viewModel.indicationAreaTranslationX.collect { translationX -> + indicationArea.translationX = translationX + ambientIndicationArea?.translationX = translationX + } } - } - launch { - viewModel.alpha.collect { alpha -> - view.importantForAccessibility = - if (alpha == 0f) { - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - } else { - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + launch { + combine( + viewModel.isIndicationAreaPadded, + configurationBasedDimensions.map { it.indicationAreaPaddingPx }, + ) { isPadded, paddingIfPaddedPx -> + if (isPadded) { + paddingIfPaddedPx + } else { + 0 + } + } + .collect { paddingPx -> + indicationArea.setPadding(paddingPx, 0, paddingPx, 0) } - - ambientIndicationArea?.alpha = alpha - indicationArea.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 + launch { + configurationBasedDimensions + .map { it.defaultBurnInPreventionYOffsetPx } + .flatMapLatest { defaultBurnInOffsetY -> + viewModel.indicationAreaTranslationY(defaultBurnInOffsetY) + } + .collect { translationY -> + indicationArea.translationY = translationY + ambientIndicationArea?.translationY = translationY + } } - } - launch { - combine( - viewModel.isIndicationAreaPadded, - configurationBasedDimensions.map { it.indicationAreaPaddingPx }, - ) { isPadded, paddingIfPaddedPx -> - if (isPadded) { - paddingIfPaddedPx - } else { - 0 + launch { + configurationBasedDimensions.collect { dimensions -> + indicationText.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + dimensions.indicationTextSizePx.toFloat(), + ) + indicationTextBottom.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + dimensions.indicationTextSizePx.toFloat(), + ) + + startButton.updateLayoutParams<ViewGroup.LayoutParams> { + width = dimensions.buttonSizePx.width + height = dimensions.buttonSizePx.height + } + endButton.updateLayoutParams<ViewGroup.LayoutParams> { + width = dimensions.buttonSizePx.width + height = dimensions.buttonSizePx.height } } - .collect { paddingPx -> - indicationArea.setPadding(paddingPx, 0, paddingPx, 0) - } - } + } - launch { - configurationBasedDimensions - .map { it.defaultBurnInPreventionYOffsetPx } - .flatMapLatest { defaultBurnInOffsetY -> - viewModel.indicationAreaTranslationY(defaultBurnInOffsetY) - } - .collect { translationY -> - indicationArea.translationY = translationY - ambientIndicationArea?.translationY = translationY + launch { + viewModel.settingsMenuViewModel.isVisible.distinctUntilChanged().collect { + isVisible -> + settingsMenu.animateVisibility(visible = isVisible) + if (isVisible) { + vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Activated) + settingsMenu.setOnTouchListener( + KeyguardSettingsButtonOnTouchListener( + view = settingsMenu, + viewModel = viewModel.settingsMenuViewModel, + ) + ) + IconViewBinder.bind( + icon = viewModel.settingsMenuViewModel.icon, + view = settingsMenu.requireViewById(R.id.icon), + ) + TextViewBinder.bind( + view = settingsMenu.requireViewById(R.id.text), + viewModel = viewModel.settingsMenuViewModel.text, + ) + } } - } - - launch { - configurationBasedDimensions.collect { dimensions -> - indicationText.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - dimensions.indicationTextSizePx.toFloat(), - ) - indicationTextBottom.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - dimensions.indicationTextSizePx.toFloat(), - ) + } - startButton.updateLayoutParams<ViewGroup.LayoutParams> { - width = dimensions.buttonSizePx.width - height = dimensions.buttonSizePx.height - } - endButton.updateLayoutParams<ViewGroup.LayoutParams> { - width = dimensions.buttonSizePx.width - height = dimensions.buttonSizePx.height + // activityStarter will only be null when rendering the preview that + // shows up in the Wallpaper Picker app. If we do that, then the + // settings menu should never be visible. + if (activityStarter != null) { + launch { + viewModel.settingsMenuViewModel.shouldOpenSettings + .filter { it } + .collect { + navigateToLockScreenSettings( + activityStarter = activityStarter, + view = settingsMenu, + ) + viewModel.settingsMenuViewModel.onSettingsShown() + } } } } } - } return object : Binding { override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> { @@ -253,6 +314,10 @@ object KeyguardBottomAreaViewBinder { override fun shouldConstrainToTopOfLockIcon(): Boolean = viewModel.shouldConstrainToTopOfLockIcon() + + override fun destroy() { + disposableHandle.dispose() + } } } @@ -265,7 +330,7 @@ object KeyguardBottomAreaViewBinder { vibratorHelper: VibratorHelper?, ) { if (!viewModel.isVisible) { - view.isVisible = false + view.isInvisible = true return } @@ -342,7 +407,7 @@ object KeyguardBottomAreaViewBinder { if (viewModel.isClickable) { if (viewModel.useLongPress) { view.setOnTouchListener( - OnTouchListener( + KeyguardQuickAffordanceOnTouchListener( view, viewModel, messageDisplayer, @@ -372,187 +437,21 @@ object KeyguardBottomAreaViewBinder { .collect { view.alpha = it } } - private class OnTouchListener( - private val view: View, - private val viewModel: KeyguardQuickAffordanceViewModel, - private val messageDisplayer: (Int) -> Unit, - private val vibratorHelper: VibratorHelper?, - private val falsingManager: FalsingManager?, - ) : View.OnTouchListener { - - private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() - private var longPressAnimator: ViewPropertyAnimator? = null - - @SuppressLint("ClickableViewAccessibility") - override fun onTouch(v: View?, event: MotionEvent?): Boolean { - return when (event?.actionMasked) { - MotionEvent.ACTION_DOWN -> - if (viewModel.configKey != null) { - if (isUsingAccurateTool(event)) { - // For accurate tool types (stylus, mouse, etc.), we don't require a - // long-press. - } else { - // When not using a stylus, we require a long-press to activate the - // quick affordance, mostly to do "falsing" (e.g. protect from false - // clicks in the pocket/bag). - longPressAnimator = - view - .animate() - .scaleX(PRESSED_SCALE) - .scaleY(PRESSED_SCALE) - .setDuration(longPressDurationMs) - .withEndAction { - if ( - falsingManager - ?.isFalseLongTap( - FalsingManager.MODERATE_PENALTY - ) == false - ) { - dispatchClick(viewModel.configKey) - } - cancel() - } - } - true - } else { - false - } - MotionEvent.ACTION_MOVE -> { - if (!isUsingAccurateTool(event)) { - // Moving too far while performing a long-press gesture cancels that - // gesture. - val distanceMoved = distanceMoved(event) - if (distanceMoved > ViewConfiguration.getTouchSlop()) { - cancel() - } - } - true - } - MotionEvent.ACTION_UP -> { - if (isUsingAccurateTool(event)) { - // When using an accurate tool type (stylus, mouse, etc.), we don't require - // a long-press gesture to activate the quick affordance. Therefore, lifting - // the pointer performs a click. - if ( - viewModel.configKey != null && - distanceMoved(event) <= ViewConfiguration.getTouchSlop() && - falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false - ) { - dispatchClick(viewModel.configKey) - } - } else { - // When not using a stylus, lifting the finger/pointer will actually cancel - // the long-press gesture. Calling cancel after the quick affordance was - // already long-press activated is a no-op, so it's safe to call from here. - cancel( - onAnimationEnd = - if (event.eventTime - event.downTime < longPressDurationMs) { - Runnable { - messageDisplayer.invoke( - R.string.keyguard_affordance_press_too_short - ) - val amplitude = - view.context.resources - .getDimensionPixelSize( - R.dimen.keyguard_affordance_shake_amplitude - ) - .toFloat() - val shakeAnimator = - ObjectAnimator.ofFloat( - view, - "translationX", - -amplitude / 2, - amplitude / 2, - ) - shakeAnimator.duration = - ShakeAnimationDuration.inWholeMilliseconds - shakeAnimator.interpolator = - CycleInterpolator(ShakeAnimationCycles) - shakeAnimator.start() - - vibratorHelper?.vibrate(Vibrations.Shake) - } - } else { - null - } - ) - } - true - } - MotionEvent.ACTION_CANCEL -> { - cancel() - true - } - else -> false - } - } - - private fun dispatchClick( - configKey: String, - ) { - view.setOnClickListener { - vibratorHelper?.vibrate( - if (viewModel.isActivated) { - Vibrations.Activated - } else { - Vibrations.Deactivated - } - ) - viewModel.onClicked( - KeyguardQuickAffordanceViewModel.OnClickedParameters( - configKey = configKey, - expandable = Expandable.fromView(view), - slotId = viewModel.slotId, - ) - ) - } - view.performClick() - view.setOnClickListener(null) - } - - private fun cancel(onAnimationEnd: Runnable? = null) { - longPressAnimator?.cancel() - longPressAnimator = null - view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd) - } - - companion object { - private const val PRESSED_SCALE = 1.5f - - /** - * Returns `true` if the tool type at the given pointer index is an accurate tool (like - * stylus or mouse), which means we can trust it to not be a false click; `false` - * otherwise. - */ - private fun isUsingAccurateTool( - event: MotionEvent, - pointerIndex: Int = 0, - ): Boolean { - return when (event.getToolType(pointerIndex)) { - MotionEvent.TOOL_TYPE_STYLUS -> true - MotionEvent.TOOL_TYPE_MOUSE -> true - else -> false + private fun View.animateVisibility(visible: Boolean) { + animate() + .withStartAction { + if (visible) { + alpha = 0f + isVisible = true } } - - /** - * Returns the amount of distance the pointer moved since the historical record at the - * [since] index. - */ - private fun distanceMoved( - event: MotionEvent, - since: Int = 0, - ): Float { - return if (event.historySize > 0) { - sqrt( - (event.y - event.getHistoricalY(since)).pow(2) + - (event.x - event.getHistoricalX(since)).pow(2) - ) - } else { - 0f + .alpha(if (visible) 1f else 0f) + .withEndAction { + if (!visible) { + isVisible = false } } - } + .start() } private class OnClickListener( @@ -594,64 +493,28 @@ object KeyguardBottomAreaViewBinder { ) } + /** Opens the wallpaper picker screen after the device is unlocked by the user. */ + private fun navigateToLockScreenSettings( + activityStarter: ActivityStarter, + view: View, + ) { + activityStarter.startActivity( + Intent(Intent.ACTION_SET_WALLPAPER).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + view.context + .getString(R.string.config_wallpaperPickerPackage) + .takeIf { it.isNotEmpty() } + ?.let { packageName -> setPackage(packageName) } + }, + /* dismissShade= */ true, + ActivityLaunchAnimator.Controller.fromView(view), + ) + } + private data class ConfigurationBasedDimensions( val defaultBurnInPreventionYOffsetPx: Int, val indicationAreaPaddingPx: Int, val indicationTextSizePx: Int, val buttonSizePx: Size, ) - - private val ShakeAnimationDuration = 300.milliseconds - private val ShakeAnimationCycles = 5f - - object Vibrations { - - private const val SmallVibrationScale = 0.3f - private const val BigVibrationScale = 0.6f - - val Shake = - VibrationEffect.startComposition() - .apply { - val vibrationDelayMs = - (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2)) - .toInt() - val vibrationCount = ShakeAnimationCycles.toInt() * 2 - repeat(vibrationCount) { - addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - SmallVibrationScale, - vibrationDelayMs, - ) - } - } - .compose() - - val Activated = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - BigVibrationScale, - 0, - ) - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, - 0.1f, - 0, - ) - .compose() - - val Deactivated = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - BigVibrationScale, - 0, - ) - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, - 0.1f, - 0, - ) - .compose() - } } 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 deleted file mode 100644 index d85682b3bab8..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressPopupViewBinder.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.ui.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 index 86717537efd3..9cc503c07955 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt @@ -50,10 +50,7 @@ object KeyguardLongPressViewBinder { return } - viewModel.onLongPress( - x = x, - y = y, - ) + viewModel.onLongPress() } override fun onSingleTapDetected(view: View) { @@ -72,23 +69,6 @@ object KeyguardLongPressViewBinder { 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/binder/KeyguardQuickAffordanceOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt new file mode 100644 index 000000000000..779095cd1d1e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.binder + +import android.annotation.SuppressLint +import android.graphics.PointF +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewPropertyAnimator +import androidx.core.animation.CycleInterpolator +import androidx.core.animation.ObjectAnimator +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.common.ui.view.distanceFrom +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper + +class KeyguardQuickAffordanceOnTouchListener( + private val view: View, + private val viewModel: KeyguardQuickAffordanceViewModel, + private val messageDisplayer: (Int) -> Unit, + private val vibratorHelper: VibratorHelper?, + private val falsingManager: FalsingManager?, +) : View.OnTouchListener { + + private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() + private var longPressAnimator: ViewPropertyAnimator? = null + private val down: PointF by lazy { PointF() } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + return when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> + if (viewModel.configKey != null) { + down.set(event.x, event.y) + if (isUsingAccurateTool(event)) { + // For accurate tool types (stylus, mouse, etc.), we don't require a + // long-press. + } else { + // When not using a stylus, we require a long-press to activate the + // quick affordance, mostly to do "falsing" (e.g. protect from false + // clicks in the pocket/bag). + longPressAnimator = + view + .animate() + .scaleX(PRESSED_SCALE) + .scaleY(PRESSED_SCALE) + .setDuration(longPressDurationMs) + .withEndAction { + if ( + falsingManager?.isFalseLongTap( + FalsingManager.MODERATE_PENALTY + ) == false + ) { + dispatchClick(viewModel.configKey) + } + cancel() + } + } + true + } else { + false + } + MotionEvent.ACTION_MOVE -> { + if (!isUsingAccurateTool(event)) { + // Moving too far while performing a long-press gesture cancels that + // gesture. + if (event.distanceFrom(down.x, down.y) > ViewConfiguration.getTouchSlop()) { + cancel() + } + } + true + } + MotionEvent.ACTION_UP -> { + if (isUsingAccurateTool(event)) { + // When using an accurate tool type (stylus, mouse, etc.), we don't require + // a long-press gesture to activate the quick affordance. Therefore, lifting + // the pointer performs a click. + if ( + viewModel.configKey != null && + event.distanceFrom(down.x, down.y) <= + ViewConfiguration.getTouchSlop() && + falsingManager?.isFalseTap(FalsingManager.NO_PENALTY) == false + ) { + dispatchClick(viewModel.configKey) + } + } else { + // When not using a stylus, lifting the finger/pointer will actually cancel + // the long-press gesture. Calling cancel after the quick affordance was + // already long-press activated is a no-op, so it's safe to call from here. + cancel( + onAnimationEnd = + if (event.eventTime - event.downTime < longPressDurationMs) { + Runnable { + messageDisplayer.invoke( + R.string.keyguard_affordance_press_too_short + ) + val amplitude = + view.context.resources + .getDimensionPixelSize( + R.dimen.keyguard_affordance_shake_amplitude + ) + .toFloat() + val shakeAnimator = + ObjectAnimator.ofFloat( + view, + "translationX", + -amplitude / 2, + amplitude / 2, + ) + shakeAnimator.duration = + KeyguardBottomAreaVibrations.ShakeAnimationDuration + .inWholeMilliseconds + shakeAnimator.interpolator = + CycleInterpolator( + KeyguardBottomAreaVibrations.ShakeAnimationCycles + ) + shakeAnimator.start() + + vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake) + } + } else { + null + } + ) + } + true + } + MotionEvent.ACTION_CANCEL -> { + cancel() + true + } + else -> false + } + } + + private fun dispatchClick( + configKey: String, + ) { + view.setOnClickListener { + vibratorHelper?.vibrate( + if (viewModel.isActivated) { + KeyguardBottomAreaVibrations.Activated + } else { + KeyguardBottomAreaVibrations.Deactivated + } + ) + viewModel.onClicked( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = configKey, + expandable = Expandable.fromView(view), + slotId = viewModel.slotId, + ) + ) + } + view.performClick() + view.setOnClickListener(null) + } + + private fun cancel(onAnimationEnd: Runnable? = null) { + longPressAnimator?.cancel() + longPressAnimator = null + view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd) + } + + companion object { + private const val PRESSED_SCALE = 1.5f + + /** + * Returns `true` if the tool type at the given pointer index is an accurate tool (like + * stylus or mouse), which means we can trust it to not be a false click; `false` otherwise. + */ + private fun isUsingAccurateTool( + event: MotionEvent, + pointerIndex: Int = 0, + ): Boolean { + return when (event.getToolType(pointerIndex)) { + MotionEvent.TOOL_TYPE_STYLUS -> true + MotionEvent.TOOL_TYPE_MOUSE -> true + else -> false + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt new file mode 100644 index 000000000000..ad3fb637961b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsButtonOnTouchListener.kt @@ -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.keyguard.ui.binder + +import android.graphics.PointF +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import com.android.systemui.animation.view.LaunchableLinearLayout +import com.android.systemui.common.ui.view.distanceFrom +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel + +class KeyguardSettingsButtonOnTouchListener( + private val view: LaunchableLinearLayout, + private val viewModel: KeyguardSettingsMenuViewModel, +) : View.OnTouchListener { + + private val downPosition = PointF() + + override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { + when (motionEvent.actionMasked) { + MotionEvent.ACTION_DOWN -> { + view.isPressed = true + downPosition.set(motionEvent.x, motionEvent.y) + viewModel.onTouchGestureStarted() + } + MotionEvent.ACTION_UP -> { + view.isPressed = false + val distanceMoved = motionEvent.distanceFrom(downPosition.x, downPosition.y) + val isClick = distanceMoved < ViewConfiguration.getTouchSlop() + viewModel.onTouchGestureEnded(isClick) + if (isClick) { + view.performClick() + } + } + MotionEvent.ACTION_CANCEL -> { + view.isPressed = false + viewModel.onTouchGestureEnded(/* isClick= */ false) + } + } + + return true + } +} 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 a8e346477690..2d83be95183e 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 @@ -44,6 +44,8 @@ constructor( private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, private val bottomAreaInteractor: KeyguardBottomAreaInteractor, private val burnInHelperWrapper: BurnInHelperWrapper, + private val longPressViewModel: KeyguardLongPressViewModel, + val settingsMenuViewModel: KeyguardSettingsMenuViewModel, ) { data class PreviewMode( val isInPreviewMode: Boolean = false, @@ -161,6 +163,14 @@ constructor( selectedPreviewSlotId.value = slotId } + /** + * Notifies that some input gesture has started somewhere in the bottom area that's outside of + * the lock screen settings menu item pop-up. + */ + fun onTouchedOutsideLockScreenSettingsMenu() { + longPressViewModel.onTouchedOutside() + } + private fun button( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceViewModel> { @@ -225,9 +235,10 @@ constructor( isDimmed = isDimmed, slotId = slotId, ) - is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel( - slotId = slotId, - ) + is KeyguardQuickAffordanceModel.Hidden -> + KeyguardQuickAffordanceViewModel( + slotId = slotId, + ) } } 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 index d896390fd471..c73931a12455 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModel.kt @@ -17,15 +17,13 @@ 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.dagger.SysUISingleton 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. */ +@SysUISingleton class KeyguardLongPressViewModel @Inject constructor( @@ -35,35 +33,16 @@ constructor( /** 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, - ) + fun onLongPress() { + interactor.onLongPress() + } + + /** + * Notifies that some input gesture has started somewhere outside of the lock screen settings + * menu item pop-up. + */ + fun onTouchedOutside() { + interactor.onTouchedOutside() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.kt new file mode 100644 index 000000000000..c36da9da58a5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsMenuViewModel.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.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 + +/** Models the UI state of a keyguard settings popup menu. */ +class KeyguardSettingsMenuViewModel +@Inject +constructor( + private val interactor: KeyguardLongPressInteractor, +) { + val isVisible: Flow<Boolean> = interactor.isMenuVisible + val shouldOpenSettings: Flow<Boolean> = interactor.shouldOpenSettings + + val icon: Icon = + Icon.Resource( + res = R.drawable.ic_palette, + contentDescription = null, + ) + + val text: Text = + Text.Resource( + res = R.string.lock_screen_settings, + ) + + fun onTouchGestureStarted() { + interactor.onMenuTouchGestureStarted() + } + + fun onTouchGestureEnded(isClick: Boolean) { + interactor.onMenuTouchGestureEnded( + isClick = isClick, + ) + } + + fun onSettingsShown() { + interactor.onSettingsShown() + } +} 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 deleted file mode 100644 index 0571b05b4751..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSettingsPopupMenuViewModel.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.ui.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/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt index 9d2d3553db6d..faaa205b15c2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -18,7 +18,7 @@ package com.android.systemui.log.table import android.os.Trace import com.android.systemui.Dumpable -import com.android.systemui.plugins.util.RingBuffer +import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.text.SimpleDateFormat diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index ce690e239da0..57b479e6899f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -26,8 +26,6 @@ import com.android.systemui.R; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; -import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; -import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; @@ -35,7 +33,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -public interface QSHost extends PanelInteractor, CustomTileAddedRepository { +public interface QSHost { String TILES_SETTING = Settings.Secure.QS_TILES; int POSITION_AT_END = -1; @@ -75,7 +73,11 @@ public interface QSHost extends PanelInteractor, CustomTileAddedRepository { * @see QSFactory#createTileView */ QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView); - /** Create a {@link QSTile} of a {@code tileSpec} type. */ + /** Create a {@link QSTile} of a {@code tileSpec} type. + * + * This should only be called by classes that need to create one-off instances of tiles. + * Do not use to create {@code custom} tiles without explicitly taking care of its lifecycle. + */ QSTile createTile(String tileSpec); /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt new file mode 100644 index 000000000000..14acb4b3ccf4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt @@ -0,0 +1,226 @@ +/* + * 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.qs + +import android.content.ComponentName +import android.content.Context +import androidx.annotation.GuardedBy +import com.android.internal.logging.InstanceId +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTileView +import com.android.systemui.qs.external.TileServiceRequestController +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.shared.TileSpec +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +/** + * Adapter to determine what real class to use for classes that depend on [QSHost]. + * + * * When [Flags.QS_PIPELINE_NEW_HOST] is off, all calls will be routed to [QSTileHost]. + * * When [Flags.QS_PIPELINE_NEW_HOST] is on, calls regarding the current set of tiles will be + * routed to [CurrentTilesInteractor]. Other calls (like [warn]) will still be routed to + * [QSTileHost]. + * + * This routing also includes dumps. + */ +@SysUISingleton +class QSHostAdapter +@Inject +constructor( + private val qsTileHost: QSTileHost, + private val interactor: CurrentTilesInteractor, + private val context: Context, + private val tileServiceRequestControllerBuilder: TileServiceRequestController.Builder, + @Application private val scope: CoroutineScope, + private val featureFlags: FeatureFlags, + dumpManager: DumpManager, +) : QSHost { + + companion object { + private const val TAG = "QSTileHost" + } + + private val useNewHost = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) + + @GuardedBy("callbacksMap") private val callbacksMap = mutableMapOf<QSHost.Callback, Job>() + + init { + scope.launch { tileServiceRequestControllerBuilder.create(this@QSHostAdapter).init() } + // Redirect dump to the correct host (needed for CTS tests) + dumpManager.registerCriticalDumpable( + TAG, + if (useNewHost) interactor else qsTileHost + ) + } + + override fun getTiles(): Collection<QSTile> { + return if (useNewHost) { + interactor.currentQSTiles + } else { + qsTileHost.getTiles() + } + } + + override fun getSpecs(): List<String> { + return if (useNewHost) { + interactor.currentTilesSpecs.map { it.spec } + } else { + qsTileHost.getSpecs() + } + } + + override fun removeTile(spec: String) { + if (useNewHost) { + interactor.removeTiles(listOf(TileSpec.create(spec))) + } else { + qsTileHost.removeTile(spec) + } + } + + override fun addCallback(callback: QSHost.Callback) { + if (useNewHost) { + val job = + scope.launch { + interactor.currentTiles.collect { callback.onTilesChanged() } + } + synchronized(callbacksMap) { callbacksMap.put(callback, job) } + } else { + qsTileHost.addCallback(callback) + } + } + + override fun removeCallback(callback: QSHost.Callback) { + if (useNewHost) { + synchronized(callbacksMap) { callbacksMap.get(callback)?.cancel() } + } else { + qsTileHost.removeCallback(callback) + } + } + + override fun removeTiles(specs: Collection<String>) { + if (useNewHost) { + interactor.removeTiles(specs.map(TileSpec::create)) + } else { + qsTileHost.removeTiles(specs) + } + } + + override fun removeTileByUser(component: ComponentName) { + if (useNewHost) { + interactor.removeTiles(listOf(TileSpec.create(component))) + } else { + qsTileHost.removeTileByUser(component) + } + } + + override fun addTile(spec: String, position: Int) { + if (useNewHost) { + interactor.addTile(TileSpec.create(spec), position) + } else { + qsTileHost.addTile(spec, position) + } + } + + override fun addTile(component: ComponentName, end: Boolean) { + if (useNewHost) { + interactor.addTile( + TileSpec.create(component), + if (end) POSITION_AT_END else 0 + ) + } else { + qsTileHost.addTile(component, end) + } + } + + override fun changeTilesByUser(previousTiles: List<String>, newTiles: List<String>) { + if (useNewHost) { + interactor.setTiles(newTiles.map(TileSpec::create)) + } else { + qsTileHost.changeTilesByUser(previousTiles, newTiles) + } + } + + override fun warn(message: String?, t: Throwable?) { + qsTileHost.warn(message, t) + } + + override fun getContext(): Context { + return if (useNewHost) { + context + } else { + qsTileHost.context + } + } + + override fun getUserContext(): Context { + return if (useNewHost) { + interactor.userContext.value + } else { + qsTileHost.userContext + } + } + + override fun getUserId(): Int { + return if (useNewHost) { + interactor.userId.value + } else { + qsTileHost.userId + } + } + + override fun getUiEventLogger(): UiEventLogger { + return qsTileHost.uiEventLogger + } + + override fun createTileView( + themedContext: Context?, + tile: QSTile?, + collapsedView: Boolean + ): QSTileView { + return qsTileHost.createTileView(themedContext, tile, collapsedView) + } + + override fun createTile(tileSpec: String): QSTile? { + return qsTileHost.createTile(tileSpec) + } + + override fun addTile(spec: String) { + return addTile(spec, QSHost.POSITION_AT_END) + } + + override fun addTile(tile: ComponentName) { + return addTile(tile, false) + } + + override fun indexOf(tileSpec: String): Int { + return specs.indexOf(tileSpec) + } + + override fun getNewInstanceId(): InstanceId { + return qsTileHost.newInstanceId + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 8bbdeeda356c..0ca897373d13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -37,8 +37,9 @@ import com.android.systemui.ProtoDumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.nano.SystemUIProtoDump; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QSFactory; @@ -48,9 +49,10 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.CustomTileStatePersister; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; -import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.nano.QsTileState; +import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.AutoTileManager; @@ -85,7 +87,8 @@ import javax.inject.Provider; * This class also provides the interface for adding/removing/changing tiles. */ @SysUISingleton -public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable { +public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable, + PanelInteractor, CustomTileAddedRepository { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MAX_QS_INSTANCE_ID = 1 << 20; @@ -99,7 +102,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P private final ArrayList<String> mTileSpecs = new ArrayList<>(); private final TunerService mTunerService; private final PluginManager mPluginManager; - private final DumpManager mDumpManager; private final QSLogger mQSLogger; private final UiEventLogger mUiEventLogger; private final InstanceIdSequence mInstanceIdSequence; @@ -122,9 +124,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P // This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged private boolean mTilesListDirty = true; - private final TileServiceRequestController mTileServiceRequestController; private TileLifecycleManager.Factory mTileLifeCycleManagerFactory; + private final FeatureFlags mFeatureFlags; + @Inject public QSTileHost(Context context, QSFactory defaultFactory, @@ -132,35 +135,32 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, - DumpManager dumpManager, Optional<CentralSurfaces> centralSurfacesOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, - TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory, - UserFileManager userFileManager + UserFileManager userFileManager, + FeatureFlags featureFlags ) { mContext = context; mUserContext = context; mTunerService = tunerService; mPluginManager = pluginManager; - mDumpManager = dumpManager; mQSLogger = qsLogger; mUiEventLogger = uiEventLogger; mMainExecutor = mainExecutor; - mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this); mTileLifeCycleManagerFactory = tileLifecycleManagerFactory; mUserFileManager = userFileManager; + mFeatureFlags = featureFlags; mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); mCentralSurfacesOptional = centralSurfacesOptional; mQsFactories.add(defaultFactory); pluginManager.addPluginListener(this, QSFactory.class, true); - mDumpManager.registerDumpable(TAG, this); mUserTracker = userTracker; mSecureSettings = secureSettings; mCustomTileStatePersister = customTileStatePersister; @@ -172,7 +172,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P tunerService.addTunable(this, TILES_SETTING); // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. mAutoTiles = autoTiles.get(); - mTileServiceRequestController.init(); }); } @@ -186,8 +185,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mAutoTiles.destroy(); mTunerService.removeTunable(this); mPluginManager.removePluginListener(this); - mDumpManager.unregisterDumpable(TAG); - mTileServiceRequestController.destroy(); } @Override @@ -300,6 +297,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P if (!TILES_SETTING.equals(key)) { return; } + // Do not process tiles if the flag is enabled. + if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + return; + } if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt index 964fe7104324..3ddd9f1cfe5f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.dagger import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.qs.QSHost +import com.android.systemui.qs.QSHostAdapter import com.android.systemui.qs.QSTileHost import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedSharedPrefsRepository @@ -31,7 +32,7 @@ import dagger.Provides @Module interface QSHostModule { - @Binds fun provideQsHost(controllerImpl: QSTileHost): QSHost + @Binds fun provideQsHost(controllerImpl: QSHostAdapter): QSHost @Module companion object { @@ -39,7 +40,7 @@ interface QSHostModule { @JvmStatic fun providePanelInteractor( featureFlags: FeatureFlags, - qsHost: QSHost, + qsHost: QSTileHost, panelInteractorImpl: PanelInteractorImpl ): PanelInteractor { return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { @@ -53,7 +54,7 @@ interface QSHostModule { @JvmStatic fun provideCustomTileAddedRepository( featureFlags: FeatureFlags, - qsHost: QSHost, + qsHost: QSTileHost, customTileAddedRepository: CustomTileAddedSharedPrefsRepository ): CustomTileAddedRepository { return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt index 00f0a67dbe22..e212bc4e7f5d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt @@ -22,6 +22,8 @@ import com.android.systemui.log.LogBufferFactory import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractorImpl import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import dagger.Binds @@ -38,6 +40,11 @@ abstract class QSPipelineModule { abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository @Binds + abstract fun bindCurrentTilesInteractor( + impl: CurrentTilesInteractorImpl + ): CurrentTilesInteractor + + @Binds @IntoMap @ClassKey(PrototypeCoreStartable::class) abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt index d254e1b3d0d7..595b29a9dcb8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -32,6 +32,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -53,6 +54,8 @@ interface TileSpecRepository { * at the end of the list. * * Passing [TileSpec.Invalid] is a noop. + * + * Trying to add a tile beyond the end of the list will add it at the end. */ suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END) @@ -61,7 +64,7 @@ interface TileSpecRepository { * * Passing [TileSpec.Invalid] or a non present tile is a noop. */ - suspend fun removeTile(@UserIdInt userId: Int, tile: TileSpec) + suspend fun removeTiles(@UserIdInt userId: Int, tiles: Collection<TileSpec>) /** * Sets the list of current [tiles] for a given [userId]. @@ -106,6 +109,7 @@ constructor( } .onStart { emit(Unit) } .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } + .distinctUntilChanged() .onEach { logger.logTilesChangedInSettings(it, userId) } .map { parseTileSpecs(it, userId) } .flowOn(backgroundDispatcher) @@ -117,7 +121,7 @@ constructor( } val tilesList = loadTiles(userId).toMutableList() if (tile !in tilesList) { - if (position < 0) { + if (position < 0 || position >= tilesList.size) { tilesList.add(tile) } else { tilesList.add(position, tile) @@ -126,12 +130,12 @@ constructor( } } - override suspend fun removeTile(userId: Int, tile: TileSpec) { - if (tile == TileSpec.Invalid) { + override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) { + if (tiles.all { it == TileSpec.Invalid }) { return } val tilesList = loadTiles(userId).toMutableList() - if (tilesList.remove(tile)) { + if (tilesList.removeAll(tiles)) { storeTiles(userId, tilesList.toList()) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt new file mode 100644 index 000000000000..91c6e8b5fcb6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -0,0 +1,344 @@ +/* + * 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.qs.pipeline.domain.interactor + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.UserHandle +import com.android.systemui.Dumpable +import com.android.systemui.ProtoDumpable +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.dump.nano.SystemUIProtoDump +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.qs.QSFactory +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.external.CustomTileStatePersister +import com.android.systemui.qs.external.TileLifecycleManager +import com.android.systemui.qs.external.TileServiceKey +import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.domain.model.TileModel +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.qs.toProto +import com.android.systemui.settings.UserTracker +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.pairwise +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Interactor for retrieving the list of current QS tiles, as well as making changes to this list + * + * It is [ProtoDumpable] as it needs to be able to dump state for CTS tests. + */ +interface CurrentTilesInteractor : ProtoDumpable { + /** Current list of tiles with their corresponding spec. */ + val currentTiles: StateFlow<List<TileModel>> + + /** User for the [currentTiles]. */ + val userId: StateFlow<Int> + + /** [Context] corresponding to [userId] */ + val userContext: StateFlow<Context> + + /** List of specs corresponding to the last value of [currentTiles] */ + val currentTilesSpecs: List<TileSpec> + get() = currentTiles.value.map(TileModel::spec) + + /** List of tiles corresponding to the last value of [currentTiles] */ + val currentQSTiles: List<QSTile> + get() = currentTiles.value.map(TileModel::tile) + + /** + * Requests that a tile be added in the list of tiles for the current user. + * + * @see TileSpecRepository.addTile + */ + fun addTile(spec: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END) + + /** + * Requests that tiles be removed from the list of tiles for the current user + * + * If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and + * marked as removed. + * + * @see TileSpecRepository.removeTiles + */ + fun removeTiles(specs: Collection<TileSpec>) + + /** + * Requests that the list of tiles for the current user is changed to [specs]. + * + * If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and + * marked as removed. + * + * @see TileSpecRepository.setTiles + */ + fun setTiles(specs: List<TileSpec>) +} + +/** + * This implementation of [CurrentTilesInteractor] will try to re-use existing [QSTile] objects when + * possible, in particular: + * * It will only destroy tiles when they are not part of the list of tiles anymore + * * Platform tiles will be kept between users, with a call to [QSTile.userSwitch] + * * [CustomTile]s will only be destroyed if the user changes. + */ +@SysUISingleton +class CurrentTilesInteractorImpl +@Inject +constructor( + private val tileSpecRepository: TileSpecRepository, + private val userRepository: UserRepository, + private val customTileStatePersister: CustomTileStatePersister, + private val tileFactory: QSFactory, + private val customTileAddedRepository: CustomTileAddedRepository, + private val tileLifecycleManagerFactory: TileLifecycleManager.Factory, + private val userTracker: UserTracker, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val logger: QSPipelineLogger, + featureFlags: FeatureFlags, +) : CurrentTilesInteractor { + + private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> = + MutableStateFlow(emptyList()) + + override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow() + + // This variable should only be accessed inside the collect of `startTileCollection`. + private val specsToTiles = mutableMapOf<TileSpec, QSTile>() + + private val currentUser = MutableStateFlow(userTracker.userId) + override val userId = currentUser.asStateFlow() + + private val _userContext = MutableStateFlow(userTracker.userContext) + override val userContext = _userContext.asStateFlow() + + init { + if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + startTileCollection() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun startTileCollection() { + scope.launch { + userRepository.selectedUserInfo + .flatMapLatest { user -> + currentUser.value = user.id + _userContext.value = userTracker.userContext + tileSpecRepository.tilesSpecs(user.id).map { user.id to it } + } + .distinctUntilChanged() + .pairwise(-1 to emptyList()) + .flowOn(backgroundDispatcher) + .collect { (old, new) -> + val newTileList = new.second + val userChanged = old.first != new.first + val newUser = new.first + + // Destroy all tiles that are not in the new set + specsToTiles + .filter { it.key !in newTileList } + .forEach { entry -> + logger.logTileDestroyed( + entry.key, + if (userChanged) { + QSPipelineLogger.TileDestroyedReason + .TILE_NOT_PRESENT_IN_NEW_USER + } else { + QSPipelineLogger.TileDestroyedReason.TILE_REMOVED + } + ) + entry.value.destroy() + } + // MutableMap will keep the insertion order + val newTileMap = mutableMapOf<TileSpec, QSTile>() + + newTileList.forEach { tileSpec -> + if (tileSpec !in newTileMap) { + val newTile = + if (tileSpec in specsToTiles) { + processExistingTile( + tileSpec, + specsToTiles.getValue(tileSpec), + userChanged, + newUser + ) + ?: createTile(tileSpec) + } else { + createTile(tileSpec) + } + if (newTile != null) { + newTileMap[tileSpec] = newTile + } + } + } + + val resolvedSpecs = newTileMap.keys.toList() + specsToTiles.clear() + specsToTiles.putAll(newTileMap) + _currentSpecsAndTiles.value = newTileMap.map { TileModel(it.key, it.value) } + if (resolvedSpecs != newTileList) { + // There were some tiles that couldn't be created. Change the value in the + // repository + launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } + } + } + } + } + + override fun addTile(spec: TileSpec, position: Int) { + scope.launch { + tileSpecRepository.addTile(userRepository.getSelectedUserInfo().id, spec, position) + } + } + + override fun removeTiles(specs: Collection<TileSpec>) { + val currentSpecsCopy = currentTilesSpecs.toSet() + val user = currentUser.value + // intersect: tiles that are there and are being removed + val toFree = currentSpecsCopy.intersect(specs).filterIsInstance<TileSpec.CustomTileSpec>() + toFree.forEach { onCustomTileRemoved(it.componentName, user) } + if (currentSpecsCopy.intersect(specs).isNotEmpty()) { + // We don't want to do the call to set in case getCurrentTileSpecs is not the most + // up to date for this user. + scope.launch { tileSpecRepository.removeTiles(user, specs) } + } + } + + override fun setTiles(specs: List<TileSpec>) { + val currentSpecsCopy = currentTilesSpecs + val user = currentUser.value + if (currentSpecsCopy != specs) { + // minus: tiles that were there but are not there anymore + val toFree = currentSpecsCopy.minus(specs).filterIsInstance<TileSpec.CustomTileSpec>() + toFree.forEach { onCustomTileRemoved(it.componentName, user) } + scope.launch { tileSpecRepository.setTiles(user, specs) } + } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("CurrentTileInteractorImpl:") + pw.println("User: ${userId.value}") + currentTiles.value + .map { it.tile } + .filterIsInstance<Dumpable>() + .forEach { it.dump(pw, args) } + } + + override fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) { + val data = + currentTiles.value.map { it.tile.state }.mapNotNull { it.toProto() }.toTypedArray() + systemUIProtoDump.tiles = data + } + + private fun onCustomTileRemoved(componentName: ComponentName, userId: Int) { + val intent = Intent().setComponent(componentName) + val lifecycleManager = tileLifecycleManagerFactory.create(intent, UserHandle.of(userId)) + lifecycleManager.onStopListening() + lifecycleManager.onTileRemoved() + customTileStatePersister.removeState(TileServiceKey(componentName, userId)) + customTileAddedRepository.setTileAdded(componentName, userId, false) + lifecycleManager.flushMessagesAndUnbind() + } + + private suspend fun createTile(spec: TileSpec): QSTile? { + val tile = withContext(mainDispatcher) { tileFactory.createTile(spec.spec) } + if (tile == null) { + logger.logTileNotFoundInFactory(spec) + return null + } else { + tile.tileSpec = spec.spec + return if (!tile.isAvailable) { + logger.logTileDestroyed( + spec, + QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE, + ) + tile.destroy() + null + } else { + logger.logTileCreated(spec) + tile + } + } + } + + private fun processExistingTile( + tileSpec: TileSpec, + qsTile: QSTile, + userChanged: Boolean, + user: Int, + ): QSTile? { + return when { + !qsTile.isAvailable -> { + logger.logTileDestroyed( + tileSpec, + QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE + ) + qsTile.destroy() + null + } + // Tile is in the current list of tiles and available. + // We have a handful of different cases + qsTile !is CustomTile -> { + // The tile is not a custom tile. Make sure they are reset to the correct user + qsTile.removeCallbacks() + if (userChanged) { + qsTile.userSwitch(user) + logger.logTileUserChanged(tileSpec, user) + } + qsTile + } + qsTile.user == user -> { + // The tile is a custom tile for the same user, just return it + qsTile.removeCallbacks() + qsTile + } + else -> { + // The tile is a custom tile and the user has changed. Destroy it + qsTile.destroy() + logger.logTileDestroyed( + tileSpec, + QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED + ) + null + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt index 7c61e7108265..e2381ecd8b97 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardSettingsPopupMenuModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt @@ -5,26 +5,28 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ -package com.android.systemui.keyguard.domain.model +package com.android.systemui.qs.pipeline.domain.model -import com.android.systemui.common.shared.model.Position +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.pipeline.shared.TileSpec -/** 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, -) +/** + * Container for a [tile] and its [spec]. The following must be true: + * ``` + * spec.spec == tile.tileSpec + * ``` + */ +data class TileModel(val spec: TileSpec, val tile: QSTile) { + init { + check(spec.spec == tile.tileSpec) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt index 69d8248a11f5..89408006c300 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt @@ -93,7 +93,7 @@ constructor( private fun performRemove(args: List<String>, spec: TileSpec) { val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id - scope.launch { tileSpecRepository.removeTile(user, spec) } + scope.launch { tileSpecRepository.removeTiles(user, listOf(spec)) } } override fun help(pw: PrintWriter) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt index c691c2f668ad..af1cd0995a21 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt @@ -66,6 +66,10 @@ sealed class TileSpec private constructor(open val spec: String) { } } + fun create(component: ComponentName): CustomTileSpec { + return CustomTileSpec(CustomTile.toSpec(component), component) + } + private val String.isCustomTileSpec: Boolean get() = startsWith(CustomTile.PREFIX) diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt index 200f7431e906..767ce919d027 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt @@ -73,4 +73,59 @@ constructor( { "Tiles changed in settings for user $int1: $str1" } ) } + + /** Log when a tile is destroyed and its reason for destroying. */ + fun logTileDestroyed(spec: TileSpec, reason: TileDestroyedReason) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { + str1 = spec.toString() + str2 = reason.readable + }, + { "Tile $str1 destroyed. Reason: $str2" } + ) + } + + /** Log when a tile is created. */ + fun logTileCreated(spec: TileSpec) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { str1 = spec.toString() }, + { "Tile $str1 created" } + ) + } + + /** Ĺog when trying to create a tile, but it's not found in the factory. */ + fun logTileNotFoundInFactory(spec: TileSpec) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.VERBOSE, + { str1 = spec.toString() }, + { "Tile $str1 not found in factory" } + ) + } + + /** Log when the user is changed for a platform tile. */ + fun logTileUserChanged(spec: TileSpec, user: Int) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.VERBOSE, + { + str1 = spec.toString() + int1 = user + }, + { "User changed to $int1 for tile $str1" } + ) + } + + /** Reasons for destroying an existing tile. */ + enum class TileDestroyedReason(val readable: String) { + TILE_REMOVED("Tile removed from current set"), + CUSTOM_TILE_USER_CHANGED("User changed for custom tile"), + NEW_TILE_NOT_AVAILABLE("New tile not available"), + EXISTING_TILE_NOT_AVAILABLE("Existing tile not available"), + TILE_NOT_PRESENT_IN_NEW_USER("Tile not present in new user"), + } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 72286f175671..3711a2f39b7b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -162,7 +162,7 @@ open class UserTrackerImpl internal constructor( private fun registerUserSwitchObserver() { iActivityManager.registerUserSwitchObserver(object : UserSwitchObserver() { override fun onBeforeUserSwitching(newUserId: Int) { - setUserIdInternal(newUserId) + handleBeforeUserSwitching(newUserId) } override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) { @@ -180,6 +180,10 @@ open class UserTrackerImpl internal constructor( }, TAG) } + protected open fun handleBeforeUserSwitching(newUserId: Int) { + setUserIdInternal(newUserId) + } + @WorkerThread protected open fun handleUserSwitching(newUserId: Int) { Assert.isNotMainThread() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt index 754036d3baa9..b8bd95c89ec8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt @@ -14,9 +14,9 @@ package com.android.systemui.shade import android.view.MotionEvent +import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.plugins.util.RingBuffer import java.text.SimpleDateFormat import java.util.Locale diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 79d3b26e01c7..aedd9762a601 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -22,10 +22,6 @@ import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; -import static androidx.constraintlayout.widget.ConstraintSet.END; -import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE; @@ -73,12 +69,6 @@ import android.os.Trace; import android.os.UserManager; import android.os.VibrationEffect; import android.provider.Settings; -import android.transition.ChangeBounds; -import android.transition.Transition; -import android.transition.TransitionListenerAdapter; -import android.transition.TransitionManager; -import android.transition.TransitionSet; -import android.transition.TransitionValues; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; @@ -100,8 +90,6 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; -import androidx.constraintlayout.widget.ConstraintSet; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; @@ -162,7 +150,7 @@ import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; -import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.FalsingManager.FalsingTapListener; import com.android.systemui.plugins.qs.QS; @@ -299,11 +287,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1); private static final Rect EMPTY_RECT = new Rect(); /** - * Duration to use for the animator when the keyguard status view alignment changes, and a - * custom clock animation is in use. - */ - private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000; - /** * Whether the Shade should animate to reflect Back gesture progress. * To minimize latency at runtime, we cache this, else we'd be reading it every time * updateQsExpansion() is called... and it's called very often. @@ -550,8 +533,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final KeyguardMediaController mKeyguardMediaController; - private boolean mStatusViewCentered = true; - private final Optional<KeyguardUnfoldTransition> mKeyguardUnfoldTransition; private final Optional<NotificationPanelUnfoldAnimationController> mNotificationPanelUnfoldAnimationController; @@ -682,35 +663,29 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump step.getTransitionState() == TransitionState.RUNNING; }; - private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener = - new TransitionListenerAdapter() { - @Override - public void onTransitionCancel(Transition transition) { - mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); - } - - @Override - public void onTransitionEnd(Transition transition) { - mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); - } - }; + private final ActivityStarter mActivityStarter; @Inject public NotificationPanelViewController(NotificationPanelView view, @Main Handler handler, LayoutInflater layoutInflater, FeatureFlags featureFlags, - NotificationWakeUpCoordinator coordinator, PulseExpansionHandler pulseExpansionHandler, + NotificationWakeUpCoordinator coordinator, + PulseExpansionHandler pulseExpansionHandler, DynamicPrivacyController dynamicPrivacyController, - KeyguardBypassController bypassController, FalsingManager falsingManager, + KeyguardBypassController bypassController, + FalsingManager falsingManager, FalsingCollector falsingCollector, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, StatusBarWindowStateController statusBarWindowStateController, NotificationShadeWindowController notificationShadeWindowController, DozeLog dozeLog, - DozeParameters dozeParameters, CommandQueue commandQueue, VibratorHelper vibratorHelper, - LatencyTracker latencyTracker, PowerManager powerManager, + DozeParameters dozeParameters, + CommandQueue commandQueue, + VibratorHelper vibratorHelper, + LatencyTracker latencyTracker, + PowerManager powerManager, AccessibilityManager accessibilityManager, @DisplayId int displayId, KeyguardUpdateMonitor keyguardUpdateMonitor, MetricsLogger metricsLogger, @@ -771,7 +746,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump Provider<MultiShadeInteractor> multiShadeInteractorProvider, DumpManager dumpManager, KeyguardLongPressViewModel keyguardLongPressViewModel, - KeyguardInteractor keyguardInteractor) { + KeyguardInteractor keyguardInteractor, + ActivityStarter activityStarter) { mInteractionJankMonitor = interactionJankMonitor; keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override @@ -952,6 +928,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return Unit.INSTANCE; }, mFalsingManager); + mActivityStarter = activityStarter; onFinishInflate(); keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener( new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() { @@ -1313,9 +1290,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump keyguardStatusView = (KeyguardStatusView) mLayoutInflater.inflate( R.layout.keyguard_status_view, mNotificationContainerParent, false); mNotificationContainerParent.addView(keyguardStatusView, statusIndex); - // When it's reinflated, this is centered by default. If it shouldn't be, this will update - // below when resources are updated. - mStatusViewCentered = true; attachSplitShadeMediaPlayerContainer( keyguardStatusView.findViewById(R.id.status_view_media_container)); @@ -1394,7 +1368,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mLockIconViewController, stringResourceId -> mKeyguardIndicationController.showTransientIndication(stringResourceId), - mVibratorHelper); + mVibratorHelper, + mActivityStarter); } @VisibleForTesting @@ -1609,68 +1584,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void updateKeyguardStatusViewAlignment(boolean animate) { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); - if (mStatusViewCentered != shouldBeCentered) { - mStatusViewCentered = shouldBeCentered; - ConstraintSet constraintSet = new ConstraintSet(); - constraintSet.clone(mNotificationContainerParent); - int statusConstraint = shouldBeCentered ? PARENT_ID : R.id.qs_edge_guideline; - constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); - if (animate) { - mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); - ChangeBounds transition = new ChangeBounds(); - if (mSplitShadeEnabled) { - // Excluding media from the transition on split-shade, as it doesn't transition - // horizontally properly. - transition.excludeTarget(R.id.status_view_media_container, true); - } - - transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - - ClockController clock = mKeyguardStatusViewController.getClockController(); - boolean customClockAnimation = clock != null - && clock.getConfig().getHasCustomPositionUpdatedAnimation(); - - if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { - // Find the clock, so we can exclude it from this transition. - FrameLayout clockContainerView = - mView.findViewById(R.id.lockscreen_clock_view_large); - - // The clock container can sometimes be null. If it is, just fall back to the - // old animation rather than setting up the custom animations. - if (clockContainerView == null || clockContainerView.getChildCount() == 0) { - transition.addListener(mKeyguardStatusAlignmentTransitionListener); - TransitionManager.beginDelayedTransition( - mNotificationContainerParent, transition); - } else { - View clockView = clockContainerView.getChildAt(0); - - transition.excludeTarget(clockView, /* exclude= */ true); - - TransitionSet set = new TransitionSet(); - set.addTransition(transition); - - SplitShadeTransitionAdapter adapter = - new SplitShadeTransitionAdapter(mKeyguardStatusViewController); - - // Use linear here, so the actual clock can pick its own interpolator. - adapter.setInterpolator(Interpolators.LINEAR); - adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION); - adapter.addTarget(clockView); - set.addTransition(adapter); - set.addListener(mKeyguardStatusAlignmentTransitionListener); - TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); - } - } else { - transition.addListener(mKeyguardStatusAlignmentTransitionListener); - TransitionManager.beginDelayedTransition( - mNotificationContainerParent, transition); - } - } - - constraintSet.applyTo(mNotificationContainerParent); - } - mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(mStatusViewCentered)); + mKeyguardStatusViewController.updateAlignment( + mNotificationContainerParent, mSplitShadeEnabled, shouldBeCentered, animate); + mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered)); } private boolean shouldKeyguardStatusViewBeCentered() { @@ -3324,7 +3240,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ipw.print("mIsGestureNavigation="); ipw.println(mIsGestureNavigation); ipw.print("mOldLayoutDirection="); ipw.println(mOldLayoutDirection); ipw.print("mMinFraction="); ipw.println(mMinFraction); - ipw.print("mStatusViewCentered="); ipw.println(mStatusViewCentered); ipw.print("mSplitShadeFullTransitionDistance="); ipw.println(mSplitShadeFullTransitionDistance); ipw.print("mSplitShadeScrimTransitionDistance="); @@ -4925,6 +4840,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } handled |= handleTouch(event); + mShadeLog.logOnTouchEventLastReturn(event, !mDozing, handled); return !mDozing || handled; } @@ -5107,6 +5023,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } break; } + mShadeLog.logHandleTouchLastReturn(event, !mGestureWaitForTouchSlop, mTracking); return !mGestureWaitForTouchSlop || mTracking; } @@ -5117,65 +5034,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } - static class SplitShadeTransitionAdapter extends Transition { - private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; - private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; - - private final KeyguardStatusViewController mController; - - SplitShadeTransitionAdapter(KeyguardStatusViewController controller) { - mController = controller; - } - - private void captureValues(TransitionValues transitionValues) { - Rect boundsRect = new Rect(); - boundsRect.left = transitionValues.view.getLeft(); - boundsRect.top = transitionValues.view.getTop(); - boundsRect.right = transitionValues.view.getRight(); - boundsRect.bottom = transitionValues.view.getBottom(); - transitionValues.values.put(PROP_BOUNDS, boundsRect); - } - - @Override - public void captureEndValues(TransitionValues transitionValues) { - captureValues(transitionValues); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - captureValues(transitionValues); - } - - @Nullable - @Override - public Animator createAnimator(ViewGroup sceneRoot, @Nullable TransitionValues startValues, - @Nullable TransitionValues endValues) { - if (startValues == null || endValues == null) { - return null; - } - ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - - Rect from = (Rect) startValues.values.get(PROP_BOUNDS); - Rect to = (Rect) endValues.values.get(PROP_BOUNDS); - - anim.addUpdateListener(animation -> { - ClockController clock = mController.getClockController(); - if (clock == null) { - return; - } - - clock.getAnimations().onPositionUpdated(from, to, animation.getAnimatedFraction()); - }); - - return anim; - } - - @Override - public String[] getTransitionProperties() { - return TRANSITION_PROPERTIES; - } - } - private final class HeadsUpNotificationViewControllerImpl implements HeadsUpTouchHelper.HeadsUpNotificationViewController { @Override diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index fed9b8469c4b..7812f07fc59c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -16,9 +16,9 @@ package com.android.systemui.shade +import com.android.systemui.common.buffer.RingBuffer 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 diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index b31ec3319781..7cb1cbe77539 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -17,6 +17,8 @@ package com.android.systemui.shade; +import static android.view.WindowInsets.Type.ime; + import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS; @@ -463,9 +465,17 @@ public class QuickSettingsController { return (mQs != null ? mQs.getHeader().getHeight() : 0) + mPeekHeight; } + private boolean isRemoteInputActiveWithKeyboardUp() { + //TODO(b/227115380) remove the isVisible(ime()) check once isRemoteInputActive is fixed. + // The check for keyboard visibility is a temporary workaround that allows QS to expand + // even when isRemoteInputActive is mistakenly returning true. + return mRemoteInputManager.isRemoteInputActive() + && mPanelView.getRootWindowInsets().isVisible(ime()); + } + public boolean isExpansionEnabled() { return mExpansionEnabledPolicy && mExpansionEnabledAmbient - && !mRemoteInputManager.isRemoteInputActive(); + && !isRemoteInputActiveWithKeyboardUp(); } public float getTransitioningToFullShadeProgress() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index da4944c20f6e..a93183865a3f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -316,4 +316,80 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { { "QSC NotificationsClippingTopBound set to $int1 - $int2" } ) } + + fun logOnTouchEventLastReturn( + event: MotionEvent, + dozing: Boolean, + handled: Boolean, + ) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = dozing + bool2 = handled + long1 = event.eventTime + long2 = event.downTime + int1 = event.action + int2 = event.classification + double1 = event.y.toDouble() + }, + { + "NPVC onTouchEvent last return: !mDozing: $bool1 || handled: $bool2 " + + "\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2" + } + ) + } + + fun logHandleTouchLastReturn( + event: MotionEvent, + gestureWaitForTouchSlop: Boolean, + tracking: Boolean, + ) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = gestureWaitForTouchSlop + bool2 = tracking + long1 = event.eventTime + long2 = event.downTime + int1 = event.action + int2 = event.classification + double1 = event.y.toDouble() + }, + { + "NPVC handleTouch last return: !mGestureWaitForTouchSlop: $bool1 " + + "|| mTracking: $bool2 " + + "\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2" + } + ) + } + + fun logUpdateNotificationPanelTouchState( + disabled: Boolean, + isGoingToSleep: Boolean, + shouldControlScreenOff: Boolean, + deviceInteractive: Boolean, + isPulsing: Boolean, + isFrpActive: Boolean, + ) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = disabled + bool2 = isGoingToSleep + bool3 = shouldControlScreenOff + bool4 = deviceInteractive + str1 = isPulsing.toString() + str2 = isFrpActive.toString() + }, + { + "CentralSurfaces updateNotificationPanelTouchState set disabled to: $bool1\n" + + "isGoingToSleep: $bool2, !shouldControlScreenOff: $bool3," + + "!mDeviceInteractive: $bool4, !isPulsing: $str1, isFrpActive: $str2" + } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 765c93ed209b..9b1e2faf3b69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1127,13 +1127,7 @@ public class KeyguardIndicationController { final boolean faceAuthUnavailable = biometricSourceType == FACE && msgId == BIOMETRIC_HELP_FACE_NOT_AVAILABLE; - // TODO(b/141025588): refactor to reduce repetition of code/comments - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed - if (!mKeyguardUpdateMonitor - .isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) + if (isPrimaryAuthRequired() && !faceAuthUnavailable) { return; } @@ -1234,7 +1228,7 @@ public class KeyguardIndicationController { private void onFaceAuthError(int msgId, String errString) { CharSequence deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage(); mFaceAcquiredMessageDeferral.reset(); - if (shouldSuppressFaceError(msgId, mKeyguardUpdateMonitor)) { + if (shouldSuppressFaceError(msgId)) { mKeyguardLogger.logBiometricMessage("suppressingFaceError", msgId, errString); return; } @@ -1248,7 +1242,7 @@ public class KeyguardIndicationController { } private void onFingerprintAuthError(int msgId, String errString) { - if (shouldSuppressFingerprintError(msgId, mKeyguardUpdateMonitor)) { + if (shouldSuppressFingerprintError(msgId)) { mKeyguardLogger.logBiometricMessage("suppressingFingerprintError", msgId, errString); @@ -1257,31 +1251,19 @@ public class KeyguardIndicationController { } } - private boolean shouldSuppressFingerprintError(int msgId, - KeyguardUpdateMonitor updateMonitor) { - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed - return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) - && !isLockoutError(msgId)) + private boolean shouldSuppressFingerprintError(int msgId) { + return ((isPrimaryAuthRequired() && !isLockoutError(msgId)) || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED); } - private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) { - // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong - // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to - // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the - // check of whether non-strong biometric is allowed - return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) - && msgId != FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) + private boolean shouldSuppressFaceError(int msgId) { + return ((isPrimaryAuthRequired() && msgId != FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) || msgId == FaceManager.FACE_ERROR_CANCELED || msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS); } - @Override public void onTrustChanged(int userId) { if (!isCurrentUser(userId)) return; @@ -1355,6 +1337,16 @@ public class KeyguardIndicationController { } } + private boolean isPrimaryAuthRequired() { + // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong + // as long as primary auth, i.e. PIN/pattern/password, is required), so it's ok to + // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the + // check of whether non-strong biometric is allowed since strong biometrics can still be + // used. + return !mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + true /* isStrongBiometric */); + } + protected boolean isPluggedInAndCharging() { return mPowerPluggedIn; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java index cb4ae286d5c3..f7d37e6b1058 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java @@ -25,7 +25,6 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconContainer; @@ -35,7 +34,7 @@ import javax.inject.Inject; * Controller class for {@link NotificationShelf}. */ @NotificationRowScope -public class NotificationShelfController { +public class LegacyNotificationShelfControllerImpl implements NotificationShelfController { private final NotificationShelf mView; private final ActivatableNotificationViewController mActivatableNotificationViewController; private final KeyguardBypassController mKeyguardBypassController; @@ -44,7 +43,7 @@ public class NotificationShelfController { private AmbientState mAmbientState; @Inject - public NotificationShelfController( + public LegacyNotificationShelfControllerImpl( NotificationShelf notificationShelf, ActivatableNotificationViewController activatableNotificationViewController, KeyguardBypassController keyguardBypassController, @@ -79,56 +78,42 @@ public class NotificationShelfController { } } + @Override public NotificationShelf getView() { return mView; } + @Override public boolean canModifyColorOfNotifications() { return mAmbientState.isShadeExpanded() && !(mAmbientState.isOnKeyguard() && mKeyguardBypassController.getBypassEnabled()); } + @Override public NotificationIconContainer getShelfIcons() { return mView.getShelfIcons(); } - public @View.Visibility int getVisibility() { - return mView.getVisibility(); - } - - public void setCollapsedIcons(NotificationIconContainer notificationIcons) { - mView.setCollapsedIcons(notificationIcons); - } - + @Override public void bind(AmbientState ambientState, NotificationStackScrollLayoutController notificationStackScrollLayoutController) { mView.bind(ambientState, notificationStackScrollLayoutController); mAmbientState = ambientState; } - public int getHeight() { - return mView.getHeight(); - } - - public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { - mAmbientState = ambientState; - mView.updateState(algorithmState, ambientState); - } - + @Override public int getIntrinsicHeight() { return mView.getIntrinsicHeight(); } + @Override public void setOnActivatedListener(ActivatableNotificationView.OnActivatedListener listener) { mView.setOnActivatedListener(listener); } + @Override public void setOnClickListener(View.OnClickListener onClickListener) { mView.setOnClickListener(onClickListener); } - public int getNotGoneIndex() { - return mView.getNotGoneIndex(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 4873c9dae89a..e6715a133838 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -64,8 +64,7 @@ import java.io.PrintWriter; * A notification shelf view that is placed inside the notification scroller. It manages the * overflow icons that don't fit into the regular list anymore. */ -public class NotificationShelf extends ActivatableNotificationView implements - View.OnLayoutChangeListener, StateListener { +public class NotificationShelf extends ActivatableNotificationView implements StateListener { private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag; private static final String TAG = "NotificationShelf"; @@ -78,7 +77,6 @@ public class NotificationShelf extends ActivatableNotificationView implements private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll"); private NotificationIconContainer mShelfIcons; - private int[] mTmp = new int[2]; private boolean mHideBackground; private int mStatusBarHeight; private boolean mEnableNotificationClipping; @@ -87,7 +85,6 @@ public class NotificationShelf extends ActivatableNotificationView implements private int mPaddingBetweenElements; private int mNotGoneIndex; private boolean mHasItemsInStableShelf; - private NotificationIconContainer mCollapsedIcons; private int mScrollFastThreshold; private int mStatusBarState; private boolean mInteractive; @@ -868,10 +865,6 @@ public class NotificationShelf extends ActivatableNotificationView implements return mShelfIcons.getIconState(icon); } - private float getFullyClosedTranslation() { - return -(getIntrinsicHeight() - mStatusBarHeight) / 2; - } - @Override public boolean hasNoContentHeight() { return true; @@ -893,7 +886,6 @@ public class NotificationShelf extends ActivatableNotificationView implements @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - updateRelativeOffset(); // we always want to clip to our sides, such that nothing can draw outside of these bounds int height = getResources().getDisplayMetrics().heightPixels; @@ -903,13 +895,6 @@ public class NotificationShelf extends ActivatableNotificationView implements } } - private void updateRelativeOffset() { - if (mCollapsedIcons != null) { - mCollapsedIcons.getLocationOnScreen(mTmp); - } - getLocationOnScreen(mTmp); - } - /** * @return the index of the notification at which the shelf visually resides */ @@ -924,19 +909,6 @@ public class NotificationShelf extends ActivatableNotificationView implements } } - /** - * @return whether the shelf has any icons in it when a potential animation has finished, i.e - * if the current state would be applied right now - */ - public boolean hasItemsInStableShelf() { - return mHasItemsInStableShelf; - } - - public void setCollapsedIcons(NotificationIconContainer collapsedIcons) { - mCollapsedIcons = collapsedIcons; - mCollapsedIcons.addOnLayoutChangeListener(this); - } - @Override public void onStateChanged(int newState) { mStatusBarState = newState; @@ -983,12 +955,6 @@ public class NotificationShelf extends ActivatableNotificationView implements } @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - updateRelativeOffset(); - } - - @Override public boolean needsClippingToShelf() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt new file mode 100644 index 000000000000..bf3d47c4a9ca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt @@ -0,0 +1,54 @@ +/* + * 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.view.View +import android.view.View.OnClickListener +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView.OnActivatedListener +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.phone.NotificationIconContainer + +/** Controller interface for [NotificationShelf]. */ +interface NotificationShelfController { + /** The [NotificationShelf] controlled by this Controller. */ + val view: NotificationShelf + + /** @see ExpandableView.getIntrinsicHeight */ + val intrinsicHeight: Int + + /** Container view for icons displayed in the shelf. */ + val shelfIcons: NotificationIconContainer + + /** Whether or not the shelf can modify the color of notifications in the shade. */ + fun canModifyColorOfNotifications(): Boolean + + /** @see ActivatableNotificationView.setOnActivatedListener */ + fun setOnActivatedListener(listener: OnActivatedListener) + + /** Binds the shelf to the host [NotificationStackScrollLayout], via its Controller. */ + fun bind( + ambientState: AmbientState, + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + ) + + /** @see View.setOnClickListener */ + fun setOnClickListener(listener: OnClickListener) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt index 15ad312b413e..1631ae28bf5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.PeopleHeader import com.android.systemui.statusbar.notification.icon.ConversationIconManager @@ -40,27 +41,28 @@ import javax.inject.Inject */ @CoordinatorScope class ConversationCoordinator @Inject constructor( - private val peopleNotificationIdentifier: PeopleNotificationIdentifier, - private val conversationIconManager: ConversationIconManager, - @PeopleHeader peopleHeaderController: NodeController + private val peopleNotificationIdentifier: PeopleNotificationIdentifier, + private val conversationIconManager: ConversationIconManager, + private val highPriorityProvider: HighPriorityProvider, + @PeopleHeader private val peopleHeaderController: NodeController, ) : Coordinator { private val promotedEntriesToSummaryOfSameChannel = - mutableMapOf<NotificationEntry, NotificationEntry>() + mutableMapOf<NotificationEntry, NotificationEntry>() private val onBeforeRenderListListener = OnBeforeRenderListListener { _ -> val unimportantSummaries = promotedEntriesToSummaryOfSameChannel - .mapNotNull { (promoted, summary) -> - val originalGroup = summary.parent - when { - originalGroup == null -> null - originalGroup == promoted.parent -> null - originalGroup.parent == null -> null - originalGroup.summary != summary -> null - originalGroup.children.any { it.channel == summary.channel } -> null - else -> summary.key + .mapNotNull { (promoted, summary) -> + val originalGroup = summary.parent + when { + originalGroup == null -> null + originalGroup == promoted.parent -> null + originalGroup.parent == null -> null + originalGroup.summary != summary -> null + originalGroup.children.any { it.channel == summary.channel } -> null + else -> summary.key + } } - } conversationIconManager.setUnimportantConversations(unimportantSummaries) promotedEntriesToSummaryOfSameChannel.clear() } @@ -78,21 +80,23 @@ class ConversationCoordinator @Inject constructor( } } - val sectioner = object : NotifSectioner("People", BUCKET_PEOPLE) { + val peopleAlertingSectioner = object : NotifSectioner("People(alerting)", BUCKET_PEOPLE) { override fun isInSection(entry: ListEntry): Boolean = - isConversation(entry) + highPriorityProvider.isHighPriorityConversation(entry) - override fun getComparator() = object : NotifComparator("People") { - override fun compare(entry1: ListEntry, entry2: ListEntry): Int { - val type1 = getPeopleType(entry1) - val type2 = getPeopleType(entry2) - return type2.compareTo(type1) - } - } + override fun getComparator(): NotifComparator = notifComparator - override fun getHeaderNodeController() = - // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and peopleHeaderController - if (RankingCoordinator.SHOW_ALL_SECTIONS) peopleHeaderController else null + override fun getHeaderNodeController(): NodeController? = conversationHeaderNodeController + } + + val peopleSilentSectioner = object : NotifSectioner("People(silent)", BUCKET_PEOPLE) { + // Because the peopleAlertingSectioner is above this one, it will claim all conversations that are alerting. + // All remaining conversations must be silent. + override fun isInSection(entry: ListEntry): Boolean = isConversation(entry) + + override fun getComparator(): NotifComparator = notifComparator + + override fun getHeaderNodeController(): NodeController? = conversationHeaderNodeController } override fun attach(pipeline: NotifPipeline) { @@ -101,15 +105,27 @@ class ConversationCoordinator @Inject constructor( } private fun isConversation(entry: ListEntry): Boolean = - getPeopleType(entry) != TYPE_NON_PERSON + getPeopleType(entry) != TYPE_NON_PERSON @PeopleNotificationType private fun getPeopleType(entry: ListEntry): Int = - entry.representativeEntry?.let { - peopleNotificationIdentifier.getPeopleNotificationType(it) - } ?: TYPE_NON_PERSON + entry.representativeEntry?.let { + peopleNotificationIdentifier.getPeopleNotificationType(it) + } ?: TYPE_NON_PERSON + + private val notifComparator: NotifComparator = object : NotifComparator("People") { + override fun compare(entry1: ListEntry, entry2: ListEntry): Int { + val type1 = getPeopleType(entry1) + val type2 = getPeopleType(entry2) + return type2.compareTo(type1) + } + } + + // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and peopleHeaderController + private val conversationHeaderNodeController: NodeController? = + if (RankingCoordinator.SHOW_ALL_SECTIONS) peopleHeaderController else null - companion object { + private companion object { private const val TAG = "ConversationCoordinator" } } 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 6bb5b9218ed7..02ce0d46ead8 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 @@ -21,6 +21,7 @@ import com.android.systemui.statusbar.notification.collection.PipelineDumpable import com.android.systemui.statusbar.notification.collection.PipelineDumper import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import javax.inject.Inject /** @@ -32,6 +33,7 @@ interface NotifCoordinators : Coordinator, PipelineDumpable @CoordinatorScope class NotifCoordinatorsImpl @Inject constructor( notifPipelineFlags: NotifPipelineFlags, + sectionStyleProvider: SectionStyleProvider, dataStoreCoordinator: DataStoreCoordinator, hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, @@ -56,7 +58,7 @@ class NotifCoordinatorsImpl @Inject constructor( viewConfigCoordinator: ViewConfigCoordinator, visualStabilityCoordinator: VisualStabilityCoordinator, sensitiveContentCoordinator: SensitiveContentCoordinator, - dismissibilityCoordinator: DismissibilityCoordinator + dismissibilityCoordinator: DismissibilityCoordinator, ) : NotifCoordinators { private val mCoordinators: MutableList<Coordinator> = ArrayList() @@ -99,13 +101,20 @@ class NotifCoordinatorsImpl @Inject constructor( mCoordinators.add(dismissibilityCoordinator) // Manually add Ordered Sections - // HeadsUp > FGS > People > Alerting > Silent > Minimized > Unknown/Default - mOrderedSections.add(headsUpCoordinator.sectioner) + mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp mOrderedSections.add(appOpsCoordinator.sectioner) // ForegroundService - mOrderedSections.add(conversationCoordinator.sectioner) // People + mOrderedSections.add(conversationCoordinator.peopleAlertingSectioner) // People Alerting + mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting mOrderedSections.add(rankingCoordinator.silentSectioner) // Silent mOrderedSections.add(rankingCoordinator.minimizedSectioner) // Minimized + + sectionStyleProvider.setMinimizedSections(setOf(rankingCoordinator.minimizedSectioner)) + sectionStyleProvider.setSilentSections(listOf( + conversationCoordinator.peopleSilentSectioner, + rankingCoordinator.silentSectioner, + rankingCoordinator.minimizedSectioner, + )) } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index ea5cb308a2d0..1d37dcf13037 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -27,15 +27,12 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; -import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.AlertingHeader; import com.android.systemui.statusbar.notification.dagger.SilentHeader; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -52,7 +49,6 @@ public class RankingCoordinator implements Coordinator { public static final boolean SHOW_ALL_SECTIONS = false; private final StatusBarStateController mStatusBarStateController; private final HighPriorityProvider mHighPriorityProvider; - private final SectionStyleProvider mSectionStyleProvider; private final NodeController mSilentNodeController; private final SectionHeaderController mSilentHeaderController; private final NodeController mAlertingHeaderController; @@ -63,13 +59,11 @@ public class RankingCoordinator implements Coordinator { public RankingCoordinator( StatusBarStateController statusBarStateController, HighPriorityProvider highPriorityProvider, - SectionStyleProvider sectionStyleProvider, @AlertingHeader NodeController alertingHeaderController, @SilentHeader SectionHeaderController silentHeaderController, @SilentHeader NodeController silentNodeController) { mStatusBarStateController = statusBarStateController; mHighPriorityProvider = highPriorityProvider; - mSectionStyleProvider = sectionStyleProvider; mAlertingHeaderController = alertingHeaderController; mSilentNodeController = silentNodeController; mSilentHeaderController = silentHeaderController; @@ -78,9 +72,6 @@ public class RankingCoordinator implements Coordinator { @Override public void attach(NotifPipeline pipeline) { mStatusBarStateController.addCallback(mStatusBarStateCallback); - mSectionStyleProvider.setMinimizedSections(Collections.singleton(mMinimizedNotifSectioner)); - mSectionStyleProvider.setSilentSections( - Arrays.asList(mSilentNotifSectioner, mMinimizedNotifSectioner)); pipeline.addPreGroupFilter(mSuspendedFilter); pipeline.addPreGroupFilter(mDndVisualEffectsFilter); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java index e7ef2ec084b7..731ec80817ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java @@ -16,10 +16,13 @@ package com.android.systemui.statusbar.notification.collection.provider; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Notification; import android.app.NotificationManager; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -63,7 +66,7 @@ public class HighPriorityProvider { * A GroupEntry is considered high priority if its representativeEntry (summary) or children are * high priority */ - public boolean isHighPriority(ListEntry entry) { + public boolean isHighPriority(@Nullable ListEntry entry) { if (entry == null) { return false; } @@ -78,6 +81,36 @@ public class HighPriorityProvider { || hasHighPriorityChild(entry); } + /** + * @return true if the ListEntry is high priority conversation, else false + */ + public boolean isHighPriorityConversation(@NonNull ListEntry entry) { + final NotificationEntry notifEntry = entry.getRepresentativeEntry(); + if (notifEntry == null) { + return false; + } + + if (!isPeopleNotification(notifEntry)) { + return false; + } + + if (notifEntry.getRanking().getImportance() >= NotificationManager.IMPORTANCE_DEFAULT) { + return true; + } + + return isNotificationEntryWithAtLeastOneImportantChild(entry); + } + + private boolean isNotificationEntryWithAtLeastOneImportantChild(@NonNull ListEntry entry) { + if (!(entry instanceof GroupEntry)) { + return false; + } + final GroupEntry groupEntry = (GroupEntry) entry; + return groupEntry.getChildren().stream().anyMatch( + childEntry -> + childEntry.getRanking().getImportance() + >= NotificationManager.IMPORTANCE_DEFAULT); + } private boolean hasHighPriorityChild(ListEntry entry) { if (entry instanceof NotificationEntry @@ -93,7 +126,6 @@ public class HighPriorityProvider { } } } - return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt new file mode 100644 index 000000000000..f2216fce6fef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.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.statusbar.notification.interruption + +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision + +/** + * Wraps a [NotificationInterruptStateProvider] to convert it to the new + * [VisualInterruptionDecisionProvider] interface. + */ +@SysUISingleton +class NotificationInterruptStateProviderWrapper( + private val wrapped: NotificationInterruptStateProvider +) : VisualInterruptionDecisionProvider { + + @VisibleForTesting + enum class DecisionImpl(override val shouldInterrupt: Boolean) : Decision { + SHOULD_INTERRUPT(shouldInterrupt = true), + SHOULD_NOT_INTERRUPT(shouldInterrupt = false); + + companion object { + fun of(booleanDecision: Boolean) = + if (booleanDecision) SHOULD_INTERRUPT else SHOULD_NOT_INTERRUPT + } + } + + @VisibleForTesting + class FullScreenIntentDecisionImpl( + val originalEntry: NotificationEntry, + val originalDecision: NotificationInterruptStateProvider.FullScreenIntentDecision + ) : FullScreenIntentDecision { + override val shouldInterrupt = originalDecision.shouldLaunch + override val wouldInterruptWithoutDnd = originalDecision == NO_FSI_SUPPRESSED_ONLY_BY_DND + } + + override fun addSuppressor(suppressor: NotificationInterruptSuppressor) { + wrapped.addSuppressor(suppressor) + } + + override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision = + wrapped.checkHeadsUp(entry, /* log= */ false).let { DecisionImpl.of(it) } + + override fun makeAndLogHeadsUpDecision(entry: NotificationEntry): Decision = + wrapped.checkHeadsUp(entry, /* log= */ true).let { DecisionImpl.of(it) } + + override fun makeUnloggedFullScreenIntentDecision(entry: NotificationEntry) = + wrapped.getFullScreenIntentDecision(entry).let { FullScreenIntentDecisionImpl(entry, it) } + + override fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) { + (decision as FullScreenIntentDecisionImpl).let { + wrapped.logFullScreenIntentDecision(it.originalEntry, it.originalDecision) + } + } + + override fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision = + wrapped.shouldBubbleUp(entry).let { DecisionImpl.of(it) } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt new file mode 100644 index 000000000000..c0f4fcda56bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt @@ -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.statusbar.notification.interruption + +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** + * Decides whether a notification should visually interrupt the user in various ways. + * + * These include displaying the notification as heads-up (peeking while the device is awake or + * pulsing while the device is dozing), displaying the notification as a bubble, and launching a + * full-screen intent for the notification. + */ +interface VisualInterruptionDecisionProvider { + /** + * Represents the decision to visually interrupt or not. + * + * Used for heads-up and bubble decisions; subclassed by [FullScreenIntentDecision] for + * full-screen intent decisions. + * + * @property[shouldInterrupt] whether a visual interruption should be triggered + */ + interface Decision { + val shouldInterrupt: Boolean + } + + /** + * Represents the decision to launch a full-screen intent for a notification or not. + * + * @property[wouldInterruptWithoutDnd] whether a full-screen intent should not be launched only + * because Do Not Disturb has suppressed it + */ + interface FullScreenIntentDecision : Decision { + val wouldInterruptWithoutDnd: Boolean + } + + /** + * Adds a [component][suppressor] that can suppress visual interruptions. + * + * This class may call suppressors in any order. + * + * @param[suppressor] the suppressor to add + */ + fun addSuppressor(suppressor: NotificationInterruptSuppressor) + + /** + * Decides whether a [notification][entry] should display as heads-up or not, but does not log + * that decision. + * + * @param[entry] the notification that this decision is about + * @return the decision to display that notification as heads-up or not + */ + fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision + + /** + * Decides whether a [notification][entry] should display as heads-up or not, and logs that + * decision. + * + * If the device is awake, the decision will consider whether the notification should "peek" + * (slide in from the top of the screen over the current activity). + * + * If the device is dozing, the decision will consider whether the notification should "pulse" + * (wake the screen up and display the ambient view of the notification). + * + * @see[makeUnloggedHeadsUpDecision] + * + * @param[entry] the notification that this decision is about + * @return the decision to display that notification as heads-up or not + */ + fun makeAndLogHeadsUpDecision(entry: NotificationEntry): Decision + + /** + * Decides whether a [notification][entry] should launch a full-screen intent or not, but does + * not log that decision. + * + * The returned decision can be logged by passing it to [logFullScreenIntentDecision]. + * + * @see[makeAndLogHeadsUpDecision] + * + * @param[entry] the notification that this decision is about + * @return the decision to launch a full-screen intent for that notification or not + */ + fun makeUnloggedFullScreenIntentDecision(entry: NotificationEntry): FullScreenIntentDecision + + /** + * Logs a previous [decision] to launch a full-screen intent or not. + * + * @param[decision] the decision to log + */ + fun logFullScreenIntentDecision(decision: FullScreenIntentDecision) + + /** + * Decides whether a [notification][entry] should display as a bubble or not. + * + * @param[entry] the notification that this decision is about + * @return the decision to display that notification as a bubble or not + */ + fun makeAndLogBubbleDecision(entry: NotificationEntry): Decision +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java index af8d6ec727d1..98cd84dde199 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/NotificationShelfComponent.java @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.notification.row.dagger; +import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl; import com.android.systemui.statusbar.NotificationShelf; -import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import dagger.Binds; @@ -46,7 +46,8 @@ public interface NotificationShelfComponent { * Creates a NotificationShelfController. */ @NotificationRowScope - NotificationShelfController getNotificationShelfController(); + LegacyNotificationShelfControllerImpl getNotificationShelfController(); + /** * Dagger Module that extracts interesting properties from a NotificationShelf. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/view/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/view/NotificationShelfViewBinder.kt new file mode 100644 index 000000000000..a2351578ec98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/view/NotificationShelfViewBinder.kt @@ -0,0 +1,152 @@ +/* + * 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.notification.shelf.view + +import android.view.View +import android.view.View.OnAttachStateChangeListener +import android.view.accessibility.AccessibilityManager +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.statusbar.LegacyNotificationShelfControllerImpl +import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.NotificationShelfController +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView +import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController +import com.android.systemui.statusbar.notification.row.ExpandableOutlineViewController +import com.android.systemui.statusbar.notification.row.ExpandableViewController +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.phone.NotificationIconContainer +import com.android.systemui.statusbar.phone.NotificationTapHelper +import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope +import dagger.Binds +import dagger.Module +import javax.inject.Inject + +/** Binds a [NotificationShelf] to its backend. */ +interface NotificationShelfViewBinder { + fun bind(shelf: NotificationShelf) +} + +/** + * Controller class for [NotificationShelf]. This implementation serves as a temporary wrapper + * around a [NotificationShelfViewBinder], so that external code can continue to depend on the + * [NotificationShelfController] interface. Once the [LegacyNotificationShelfControllerImpl] is + * removed, this class can go away and the ViewBinder can be used directly. + */ +@CentralSurfacesScope +class NotificationShelfViewBinderWrapperControllerImpl +@Inject +constructor( + private val shelf: NotificationShelf, + private val viewBinder: NotificationShelfViewBinder, + private val keyguardBypassController: KeyguardBypassController, + featureFlags: FeatureFlags, + private val notifTapHelperFactory: NotificationTapHelper.Factory, + private val a11yManager: AccessibilityManager, + private val falsingManager: FalsingManager, + private val falsingCollector: FalsingCollector, + private val statusBarStateController: SysuiStatusBarStateController, +) : NotificationShelfController { + + private var ambientState: AmbientState? = null + + override val view: NotificationShelf + get() = shelf + + init { + shelf.apply { + useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES)) + setSensitiveRevealAnimEndabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM)) + } + } + + fun init() { + viewBinder.bind(shelf) + + ActivatableNotificationViewController( + shelf, + notifTapHelperFactory, + ExpandableOutlineViewController(shelf, ExpandableViewController(shelf)), + a11yManager, + falsingManager, + falsingCollector, + ) + .init() + shelf.setController(this) + val onAttachStateListener = + object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + statusBarStateController.addCallback( + shelf, + SysuiStatusBarStateController.RANK_SHELF, + ) + } + + override fun onViewDetachedFromWindow(v: View) { + statusBarStateController.removeCallback(shelf) + } + } + shelf.addOnAttachStateChangeListener(onAttachStateListener) + if (shelf.isAttachedToWindow) { + onAttachStateListener.onViewAttachedToWindow(shelf) + } + } + + override val intrinsicHeight: Int + get() = shelf.intrinsicHeight + + override val shelfIcons: NotificationIconContainer + get() = shelf.shelfIcons + + override fun canModifyColorOfNotifications(): Boolean { + return (ambientState?.isShadeExpanded == true && + !(ambientState?.isOnKeyguard == true && keyguardBypassController.bypassEnabled)) + } + + override fun setOnActivatedListener(listener: ActivatableNotificationView.OnActivatedListener) { + shelf.setOnActivatedListener(listener) + } + + override fun bind( + ambientState: AmbientState, + notificationStackScrollLayoutController: NotificationStackScrollLayoutController + ) { + shelf.bind(ambientState, notificationStackScrollLayoutController) + this.ambientState = ambientState + } + + override fun setOnClickListener(listener: View.OnClickListener) { + shelf.setOnClickListener(listener) + } +} + +@Module(includes = [PrivateShelfViewBinderModule::class]) object NotificationShelfViewBinderModule + +@Module +private interface PrivateShelfViewBinderModule { + @Binds fun bindImpl(impl: NotificationShelfViewBinderImpl): NotificationShelfViewBinder +} + +@CentralSurfacesScope +private class NotificationShelfViewBinderImpl @Inject constructor() : NotificationShelfViewBinder { + override fun bind(shelf: NotificationShelf) {} +} 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 0c8e9e56b04a..7596ce08a53c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -192,6 +192,7 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.recents.utilities.Utilities; +import com.android.systemui.shade.ShadeLogger; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.CircleReveal; @@ -505,6 +506,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { /** Controller for the Shade. */ @VisibleForTesting NotificationPanelViewController mNotificationPanelViewController; + private final ShadeLogger mShadeLogger; // settings private QSPanelController mQSPanelController; @@ -738,6 +740,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { KeyguardViewMediator keyguardViewMediator, DisplayMetrics displayMetrics, MetricsLogger metricsLogger, + ShadeLogger shadeLogger, @UiBackground Executor uiBgExecutor, NotificationMediaManager notificationMediaManager, NotificationLockscreenUserManager lockScreenUserManager, @@ -830,6 +833,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mKeyguardViewMediator = keyguardViewMediator; mDisplayMetrics = displayMetrics; mMetricsLogger = metricsLogger; + mShadeLogger = shadeLogger; mUiBgExecutor = uiBgExecutor; mMediaManager = notificationMediaManager; mLockscreenUserManager = lockScreenUserManager; @@ -3672,6 +3676,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { boolean disabled = (!mDeviceInteractive && !mDozeServiceHost.isPulsing()) || goingToSleepWithoutAnimation || mDeviceProvisionedController.isFrpActive(); + mShadeLogger.logUpdateNotificationPanelTouchState(disabled, isGoingToSleep(), + !mDozeParameters.shouldControlScreenOff(), !mDeviceInteractive, + !mDozeServiceHost.isPulsing(), mDeviceProvisionedController.isFrpActive()); + mNotificationPanelViewController.setTouchAndAnimationDisabled(disabled); mNotificationIconAreaController.setAnimationsEnabled(!disabled); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt index e4227dce94e7..d433814d7ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -29,6 +29,7 @@ import com.android.systemui.R import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper @@ -57,7 +58,7 @@ constructor( } private var ambientIndicationArea: View? = null - private lateinit var binding: KeyguardBottomAreaViewBinder.Binding + private var binding: KeyguardBottomAreaViewBinder.Binding? = null private var lockIconViewController: LockIconViewController? = null /** Initializes the view. */ @@ -67,13 +68,16 @@ constructor( lockIconViewController: LockIconViewController? = null, messageDisplayer: MessageDisplayer? = null, vibratorHelper: VibratorHelper? = null, + activityStarter: ActivityStarter? = null, ) { + binding?.destroy() binding = bind( this, viewModel, falsingManager, vibratorHelper, + activityStarter, ) { messageDisplayer?.display(it) } @@ -114,12 +118,12 @@ constructor( override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - binding.onConfigurationChanged() + binding?.onConfigurationChanged() } /** Returns a list of animators to use to animate the indication areas. */ val indicationAreaAnimators: List<ViewPropertyAnimator> - get() = binding.getIndicationAreaAnimators() + get() = checkNotNull(binding).getIndicationAreaAnimators() override fun hasOverlappingRendering(): Boolean { return false @@ -139,7 +143,7 @@ constructor( super.onLayout(changed, left, top, right, bottom) findViewById<View>(R.id.ambient_indication_container)?.let { val (ambientLeft, ambientTop) = it.locationOnScreen - if (binding.shouldConstrainToTopOfLockIcon()) { + if (binding?.shouldConstrainToTopOfLockIcon() == true) { // make top of ambient indication view the bottom of the lock icon it.layout( ambientLeft, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index eb19c0d2ad71..057fa42bd347 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -193,7 +193,6 @@ public class NotificationIconAreaController implements public void setupShelf(NotificationShelfController notificationShelfController) { mShelfIcons = notificationShelfController.getShelfIcons(); - notificationShelfController.setCollapsedIcons(mNotificationIcons); } public void onDensityOrFontScaleChanged(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt index b3031515ae9d..c8174669cc65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt @@ -85,17 +85,16 @@ class ScreenOffAnimationController @Inject constructor( /** * Called when keyguard is about to be displayed and allows to perform custom animation - * - * @return A handle that can be used for cancelling the animation, if necessary */ - fun animateInKeyguard(keyguardView: View, after: Runnable): AnimatorHandle? { - animations.forEach { + fun animateInKeyguard(keyguardView: View, after: Runnable) = + animations.firstOrNull { if (it.shouldAnimateInKeyguard()) { - return@animateInKeyguard it.animateInKeyguard(keyguardView, after) + it.animateInKeyguard(keyguardView, after) + true + } else { + false } } - return null - } /** * If returns true it will disable propagating touches to apps and keyguard @@ -212,10 +211,7 @@ interface ScreenOffAnimation { fun onAlwaysOnChanged(alwaysOn: Boolean) {} fun shouldAnimateInKeyguard(): Boolean = false - fun animateInKeyguard(keyguardView: View, after: Runnable): AnimatorHandle? { - after.run() - return null - } + fun animateInKeyguard(keyguardView: View, after: Runnable) = after.run() fun shouldDelayKeyguardShow(): Boolean = false fun isKeyguardShowDelayed(): Boolean = false @@ -228,7 +224,3 @@ interface ScreenOffAnimation { fun shouldAnimateDozingChange(): Boolean = true fun shouldAnimateClockChange(): Boolean = true } - -interface AnimatorHandle { - fun cancel() -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index deb041454da4..118bfc55dd4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -160,7 +160,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( * Animates in the provided keyguard view, ending in the same position that it will be in on * AOD. */ - override fun animateInKeyguard(keyguardView: View, after: Runnable): AnimatorHandle { + override fun animateInKeyguard(keyguardView: View, after: Runnable) { shouldAnimateInKeyguard = false keyguardView.alpha = 0f keyguardView.visibility = View.VISIBLE @@ -175,36 +175,11 @@ class UnlockedScreenOffAnimationController @Inject constructor( // We animate the Y properly separately using the PropertyAnimator, as the panel // view also needs to update the end position. PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y) + PropertyAnimator.setProperty<View>(keyguardView, AnimatableProperty.Y, currentY, + AnimationProperties().setDuration(duration.toLong()), + true /* animate */) - // Start the animation on the next frame using Choreographer APIs. animateInKeyguard() is - // called while the system is busy processing lots of requests, so delaying the animation a - // frame will mitigate jank. In the event the animation is cancelled before the next frame - // is called, this callback will be removed - val keyguardAnimator = keyguardView.animate() - val nextFrameCallback = TraceUtils.namedRunnable("startAnimateInKeyguard") { - PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY, - AnimationProperties().setDuration(duration.toLong()), - true /* animate */) - keyguardAnimator.start() - } - DejankUtils.postAfterTraversal(nextFrameCallback) - val animatorHandle = object : AnimatorHandle { - private var hasCancelled = false - override fun cancel() { - if (!hasCancelled) { - DejankUtils.removeCallbacks(nextFrameCallback) - // If we're cancelled, reset state flags/listeners. The end action above - // will not be called, which is what we want since that will finish the - // screen off animation and show the lockscreen, which we don't want if we - // were cancelled. - aodUiAnimationPlaying = false - decidedToAnimateGoingToSleep = null - keyguardView.animate().setListener(null) - hasCancelled = true - } - } - } - keyguardAnimator + keyguardView.animate() .setDuration(duration.toLong()) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) @@ -230,7 +205,14 @@ class UnlockedScreenOffAnimationController @Inject constructor( } .setListener(object : AnimatorListenerAdapter() { override fun onAnimationCancel(animation: Animator?) { - animatorHandle.cancel() + // If we're cancelled, reset state flags/listeners. The end action above + // will not be called, which is what we want since that will finish the + // screen off animation and show the lockscreen, which we don't want if we + // were cancelled. + aodUiAnimationPlaying = false + decidedToAnimateGoingToSleep = null + keyguardView.animate().setListener(null) + interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) } @@ -240,7 +222,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( CUJ_SCREEN_OFF_SHOW_AOD) } }) - return animatorHandle + .start() } override fun onStartedWakingUp() { 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 0929233feb88..cc2a0ba6f798 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 @@ -33,6 +33,7 @@ import com.android.systemui.biometrics.AuthRippleView; 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.plugins.statusbar.StatusBarStateController; import com.android.systemui.privacy.OngoingPrivacyChip; import com.android.systemui.settings.UserTracker; @@ -44,12 +45,15 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationsQuickSettingsContainer; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent; +import com.android.systemui.statusbar.notification.shelf.view.NotificationShelfViewBinderModule; +import com.android.systemui.statusbar.notification.shelf.view.NotificationShelfViewBinderWrapperControllerImpl; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator; @@ -76,13 +80,15 @@ import com.android.systemui.util.settings.SecureSettings; import java.util.concurrent.Executor; import javax.inject.Named; +import javax.inject.Provider; import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; -@Module(subcomponents = StatusBarFragmentComponent.class) +@Module(subcomponents = StatusBarFragmentComponent.class, + includes = { NotificationShelfViewBinderModule.class }) public abstract class StatusBarViewModule { public static final String SHADE_HEADER = "large_screen_shade_header"; @@ -130,16 +136,24 @@ public abstract class StatusBarViewModule { @Provides @CentralSurfacesComponent.CentralSurfacesScope public static NotificationShelfController providesStatusBarWindowView( + FeatureFlags featureFlags, + Provider<NotificationShelfViewBinderWrapperControllerImpl> newImpl, NotificationShelfComponent.Builder notificationShelfComponentBuilder, NotificationShelf notificationShelf) { - NotificationShelfComponent component = notificationShelfComponentBuilder - .notificationShelf(notificationShelf) - .build(); - NotificationShelfController notificationShelfController = - component.getNotificationShelfController(); - notificationShelfController.init(); - - return notificationShelfController; + if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) { + NotificationShelfViewBinderWrapperControllerImpl impl = newImpl.get(); + impl.init(); + return impl; + } else { + NotificationShelfComponent component = notificationShelfComponentBuilder + .notificationShelf(notificationShelf) + .build(); + LegacyNotificationShelfControllerImpl notificationShelfController = + component.getNotificationShelfController(); + notificationShelfController.init(); + + return notificationShelfController; + } } /** */ 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 dce7bf21fadd..bfd133e6830c 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 @@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn /** Common interface for all of the location-based mobile icon view models. */ @@ -80,7 +79,12 @@ constructor( ) : MobileIconViewModelCommon { /** Whether or not to show the error state of [SignalDrawable] */ private val showExclamationMark: Flow<Boolean> = - iconInteractor.isDefaultDataEnabled.mapLatest { !it } + combine( + iconInteractor.isDefaultDataEnabled, + iconInteractor.isDefaultConnectionFailed, + ) { isDefaultDataEnabled, isDefaultConnectionFailed -> + !isDefaultDataEnabled || isDefaultConnectionFailed + } override val isVisible: StateFlow<Boolean> = if (!constants.hasDataCapabilities) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 08c14e743bb6..f800cf496ca9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -33,6 +33,15 @@ interface WifiRepository { /** Observable for the current wifi network activity. */ val wifiActivity: StateFlow<DataActivityModel> + + /** + * Returns true if the device is currently connected to a wifi network with a valid SSID and + * false otherwise. + */ + fun isWifiConnectedWithValidSsid(): Boolean { + val currentNetwork = wifiNetwork.value + return currentNetwork is WifiNetworkModel.Active && currentNetwork.hasValidSsid() + } } /** 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 96ab074c6e56..1a41abf031bf 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 @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor -import android.net.wifi.WifiManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -76,7 +75,7 @@ constructor( when { info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint -> info.passpointProviderFriendlyName - info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid + info.hasValidSsid() -> info.ssid else -> null } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt index 0923d7848d8c..4b33c88cea30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.wifi.shared.model +import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager import androidx.annotation.VisibleForTesting import com.android.systemui.log.table.Diffable @@ -223,6 +224,11 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { } } + /** Returns true if this network has a valid SSID and false otherwise. */ + fun hasValidSsid(): Boolean { + return ssid != null && ssid != UNKNOWN_SSID + } + override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { if (prevVal !is Active) { logFull(row) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index f1269f2b012a..673819b20e4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -38,14 +38,14 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import dagger.Lazy; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; import javax.inject.Inject; -import dagger.Lazy; - /** */ @SysUISingleton diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 48f7d924667e..f1ee1080f6c0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -16,17 +16,15 @@ package com.android.keyguard; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; +import com.android.internal.jank.InteractionJankMonitor; import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ClockController; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -45,26 +43,21 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) public class KeyguardStatusViewControllerTest extends SysuiTestCase { - @Mock - private KeyguardStatusView mKeyguardStatusView; - @Mock - private KeyguardSliceViewController mKeyguardSliceViewController; - @Mock - private KeyguardClockSwitchController mKeyguardClockSwitchController; - @Mock - private KeyguardStateController mKeyguardStateController; - @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - ConfigurationController mConfigurationController; - @Mock - DozeParameters mDozeParameters; - @Mock - ScreenOffAnimationController mScreenOffAnimationController; + @Mock private KeyguardStatusView mKeyguardStatusView; + @Mock private KeyguardSliceViewController mKeyguardSliceViewController; + @Mock private KeyguardClockSwitchController mKeyguardClockSwitchController; + @Mock private KeyguardStateController mKeyguardStateController; + @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock private ConfigurationController mConfigurationController; + @Mock private DozeParameters mDozeParameters; + @Mock private ScreenOffAnimationController mScreenOffAnimationController; + @Mock private KeyguardLogger mKeyguardLogger; + @Mock private KeyguardStatusViewController mControllerMock; + @Mock private FeatureFlags mFeatureFlags; + @Mock private InteractionJankMonitor mInteractionJankMonitor; + @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor; - @Mock - KeyguardLogger mKeyguardLogger; private KeyguardStatusViewController mController; @@ -81,7 +74,9 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { mConfigurationController, mDozeParameters, mScreenOffAnimationController, - mKeyguardLogger); + mKeyguardLogger, + mFeatureFlags, + mInteractionJankMonitor); } @Test @@ -116,12 +111,4 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { configurationListenerArgumentCaptor.getValue().onLocaleListChanged(); verify(mKeyguardClockSwitchController).onLocaleListChanged(); } - - @Test - public void getClock_forwardsToClockSwitch() { - ClockController mockClock = mock(ClockController.class); - when(mKeyguardClockSwitchController.getClock()).thenReturn(mockClock); - - assertEquals(mockClock, mController.getClockController()); - } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 08813a7fb48a..3eb9590c0b95 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -20,9 +20,12 @@ import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT; +import static android.hardware.biometrics.SensorProperties.STRENGTH_CONVENIENCE; +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.face.FaceAuthenticateOptions.AUTHENTICATE_REASON_PRIMARY_BOUNCER_SHOWN; import static android.hardware.face.FaceAuthenticateOptions.AUTHENTICATE_REASON_STARTED_WAKING_UP; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; +import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL; import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; @@ -41,6 +44,8 @@ import static com.android.systemui.statusbar.policy.DevicePostureController.DEVI import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -79,7 +84,6 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; -import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; @@ -283,33 +287,13 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Before public void setup() throws RemoteException { MockitoAnnotations.initMocks(this); - - mFaceSensorProperties = - List.of(createFaceSensorProperties(/* supportsFaceDetection = */ false)); - when(mFaceManager.isHardwareDetected()).thenReturn(true); - when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); - when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId); - mFingerprintSensorProperties = List.of( - new FingerprintSensorPropertiesInternal(1 /* sensorId */, - FingerprintSensorProperties.STRENGTH_STRONG, - 1 /* maxEnrollmentsPerUser */, - List.of(new ComponentInfoInternal("fingerprintSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, - "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)), - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - false /* resetLockoutRequiresHAT */)); - when(mFingerprintManager.isHardwareDetected()).thenReturn(true); - when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); - when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn( - mFingerprintSensorProperties); when(mUserManager.isUserUnlocked(anyInt())).thenReturn(true); when(mUserManager.isPrimaryUser()).thenReturn(true); when(mStrongAuthTracker.getStub()).thenReturn(mock(IStrongAuthTracker.Stub.class)); when(mStrongAuthTracker - .isUnlockingWithBiometricAllowed(anyBoolean() /* isStrongBiometric */)) + .isUnlockingWithBiometricAllowed(anyBoolean() /* isClass3Biometric */)) .thenReturn(true); when(mTelephonyManager.getServiceStateForSubscriber(anyInt())) .thenReturn(new ServiceState()); @@ -346,20 +330,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { anyInt()); mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); - - ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> faceCaptor = - ArgumentCaptor.forClass(IFaceAuthenticatorsRegisteredCallback.class); - verify(mFaceManager).addAuthenticatorsRegisteredCallback(faceCaptor.capture()); - mFaceAuthenticatorsRegisteredCallback = faceCaptor.getValue(); - mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); - - ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> fingerprintCaptor = - ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class); - verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( - fingerprintCaptor.capture()); - mFingerprintAuthenticatorsRegisteredCallback = fingerprintCaptor.getValue(); - mFingerprintAuthenticatorsRegisteredCallback - .onAllAuthenticatorsRegistered(mFingerprintSensorProperties); + captureAuthenticatorsRegisteredCallbacks(); + setupFaceAuth(/* isClass3 */ false); + setupFingerprintAuth(/* isClass3 */ true); verify(mBiometricManager) .registerEnabledOnKeyguardCallback(mBiometricEnabledCallbackArgCaptor.capture()); @@ -381,8 +354,64 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mAuthController.areAllFingerprintAuthenticatorsRegistered()).thenReturn(true); } + private void captureAuthenticatorsRegisteredCallbacks() throws RemoteException { + ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> faceCaptor = + ArgumentCaptor.forClass(IFaceAuthenticatorsRegisteredCallback.class); + verify(mFaceManager).addAuthenticatorsRegisteredCallback(faceCaptor.capture()); + mFaceAuthenticatorsRegisteredCallback = faceCaptor.getValue(); + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); + + ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> fingerprintCaptor = + ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class); + verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( + fingerprintCaptor.capture()); + mFingerprintAuthenticatorsRegisteredCallback = fingerprintCaptor.getValue(); + mFingerprintAuthenticatorsRegisteredCallback + .onAllAuthenticatorsRegistered(mFingerprintSensorProperties); + } + + private void setupFaceAuth(boolean isClass3) throws RemoteException { + when(mFaceManager.isHardwareDetected()).thenReturn(true); + when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); + mFaceSensorProperties = + List.of(createFaceSensorProperties(/* supportsFaceDetection = */ false, isClass3)); + when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); + assertEquals(isClass3, mKeyguardUpdateMonitor.isFaceClass3()); + } + + private void setupFingerprintAuth(boolean isClass3) throws RemoteException { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + mFingerprintSensorProperties = List.of( + createFingerprintSensorPropertiesInternal(TYPE_UDFPS_OPTICAL, isClass3)); + when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn( + mFingerprintSensorProperties); + mFingerprintAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered( + mFingerprintSensorProperties); + assertEquals(isClass3, mKeyguardUpdateMonitor.isFingerprintClass3()); + } + + private FingerprintSensorPropertiesInternal createFingerprintSensorPropertiesInternal( + @FingerprintSensorProperties.SensorType int sensorType, + boolean isClass3) { + final List<ComponentInfoInternal> componentInfo = + List.of(new ComponentInfoInternal("fingerprintSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, + "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + return new FingerprintSensorPropertiesInternal( + FINGERPRINT_SENSOR_ID, + isClass3 ? STRENGTH_STRONG : STRENGTH_CONVENIENCE, + 1 /* maxEnrollmentsPerUser */, + componentInfo, + sensorType, + true /* resetLockoutRequiresHardwareAuthToken */); + } + @NonNull - private FaceSensorPropertiesInternal createFaceSensorProperties(boolean supportsFaceDetection) { + private FaceSensorPropertiesInternal createFaceSensorProperties( + boolean supportsFaceDetection, boolean isClass3) { final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, @@ -391,10 +420,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, "vendor/version/revision" /* softwareVersion */)); - return new FaceSensorPropertiesInternal( - 0 /* id */, - FaceSensorProperties.STRENGTH_STRONG, + FACE_SENSOR_ID /* id */, + isClass3 ? STRENGTH_STRONG : STRENGTH_CONVENIENCE, 1 /* maxTemplatesAllowed */, componentInfo, FaceSensorProperties.TYPE_UNKNOWN, @@ -686,7 +714,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testUnlockingWithFaceAllowed_strongAuthTrackerUnlockingWithBiometricAllowed() { // GIVEN unlocking with biometric is allowed - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); // THEN unlocking with face and fp is allowed Assert.assertTrue(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( @@ -706,12 +734,31 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testUnlockingWithFaceAllowed_fingerprintLockout() { - // GIVEN unlocking with biometric is allowed - strongAuthNotRequired(); + public void class3FingerprintLockOut_lockOutClass1Face() throws RemoteException { + setupFaceAuth(/* isClass3 */ false); + setupFingerprintAuth(/* isClass3 */ true); + + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN fingerprint (class 3) is lock out + fingerprintErrorTemporaryLockOut(); + + // THEN unlocking with face is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FACE)); + } + + @Test + public void class3FingerprintLockOut_lockOutClass3Face() throws RemoteException { + setupFaceAuth(/* isClass3 */ true); + setupFingerprintAuth(/* isClass3 */ true); - // WHEN fingerprint is locked out - fingerprintErrorTemporaryLockedOut(); + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN fingerprint (class 3) is lock out + fingerprintErrorTemporaryLockOut(); // THEN unlocking with face is not allowed Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( @@ -719,6 +766,38 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void class3FaceLockOut_lockOutClass3Fingerprint() throws RemoteException { + setupFaceAuth(/* isClass3 */ true); + setupFingerprintAuth(/* isClass3 */ true); + + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN face (class 3) is lock out + faceAuthLockOut(); + + // THEN unlocking with fingerprint is not allowed + Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test + public void class1FaceLockOut_doesNotLockOutClass3Fingerprint() throws RemoteException { + setupFaceAuth(/* isClass3 */ false); + setupFingerprintAuth(/* isClass3 */ true); + + // GIVEN primary auth is not required by StrongAuthTracker + primaryAuthNotRequiredByStrongAuthTracker(); + + // WHEN face (class 1) is lock out + faceAuthLockOut(); + + // THEN unlocking with fingerprint is still allowed + Assert.assertTrue(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT)); + } + + @Test public void testUnlockingWithFpAllowed_strongAuthTrackerUnlockingWithBiometricNotAllowed() { // GIVEN unlocking with biometric is not allowed when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); @@ -731,10 +810,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testUnlockingWithFpAllowed_fingerprintLockout() { // GIVEN unlocking with biometric is allowed - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); - // WHEN fingerprint is locked out - fingerprintErrorTemporaryLockedOut(); + // WHEN fingerprint is lock out + fingerprintErrorTemporaryLockOut(); // THEN unlocking with fingerprint is not allowed Assert.assertFalse(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed( @@ -757,8 +836,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.onTrustChanged(true, true, getCurrentUser(), 0, null); Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser())); - // WHEN fingerprint is locked out - fingerprintErrorTemporaryLockedOut(); + // WHEN fingerprint is lock out + fingerprintErrorTemporaryLockOut(); // THEN user is NOT considered as "having trust" and bouncer cannot be skipped Assert.assertFalse(mKeyguardUpdateMonitor.getUserHasTrust(getCurrentUser())); @@ -826,7 +905,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // GIVEN udfps is supported and strong auth required for weak biometrics (face) only givenUdfpsSupported(); - strongAuthRequiredForWeakBiometricOnly(); // this allows fingerprint to run but not face + primaryAuthRequiredForWeakBiometricOnly(); // allows class3 fp to run but not class1 face // WHEN the device wakes up mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); @@ -863,7 +942,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void noFaceDetect_whenStrongAuthRequiredAndBypass_faceDetectionUnsupported() { // GIVEN bypass is enabled, face detection is NOT supported and strong auth is required lockscreenBypassIsAllowed(); - strongAuthRequiredEncrypted(); + primaryAuthRequiredEncrypted(); keyguardIsVisible(); // WHEN the device wakes up @@ -931,6 +1010,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); verifyFingerprintAuthenticateNeverCalled(); // WHEN alternate bouncer is shown + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); mKeyguardUpdateMonitor.setAlternateBouncerShowing(true); // THEN make sure FP listening begins @@ -1011,10 +1091,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testOnFaceAuthenticated_skipsFaceWhenAuthenticated() { - // test whether face will be skipped if authenticated, so the value of isStrongBiometric + // test whether face will be skipped if authenticated, so the value of isClass3Biometric // doesn't matter here mKeyguardUpdateMonitor.onFaceAuthenticated(KeyguardUpdateMonitor.getCurrentUser(), - true /* isStrongBiometric */); + true /* isClass3Biometric */); setKeyguardBouncerVisibility(true); mTestableLooper.processAllMessages(); @@ -1027,7 +1107,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); keyguardIsVisible(); - faceAuthLockedOut(); + faceAuthLockOut(); verify(mLockPatternUtils, never()).requireStrongAuth(anyInt(), anyInt()); } @@ -1050,7 +1130,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); keyguardIsVisible(); - faceAuthLockedOut(); + faceAuthLockOut(); mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, ""); @@ -1060,32 +1140,32 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testGetUserCanSkipBouncer_whenFace() { int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFaceAuthenticated(user, true /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFaceAuthenticated(user, true /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isTrue(); } @Test public void testGetUserCanSkipBouncer_whenFace_nonStrongAndDisallowed() { - when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isStrongBiometric */)) + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isClass3Biometric */)) .thenReturn(false); int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFaceAuthenticated(user, false /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFaceAuthenticated(user, false /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isFalse(); } @Test public void testGetUserCanSkipBouncer_whenFingerprint() { int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, true /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, true /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isTrue(); } @Test public void testGetUserCanSkipBouncer_whenFingerprint_nonStrongAndDisallowed() { - when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isStrongBiometric */)) + when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(false /* isClass3Biometric */)) .thenReturn(false); int user = KeyguardUpdateMonitor.getCurrentUser(); - mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, false /* isStrongBiometric */); + mKeyguardUpdateMonitor.onFingerprintAuthenticated(user, false /* isClass3Biometric */); assertThat(mKeyguardUpdateMonitor.getUserCanSkipBouncer(user)).isFalse(); } @@ -1126,9 +1206,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @BiometricConstants.LockoutMode int fingerprintLockoutMode, @BiometricConstants.LockoutMode int faceLockoutMode) { final int newUser = 12; - final boolean faceLocked = + final boolean faceLockOut = faceLockoutMode != BiometricConstants.BIOMETRIC_LOCKOUT_NONE; - final boolean fpLocked = + final boolean fpLockOut = fingerprintLockoutMode != BiometricConstants.BIOMETRIC_LOCKOUT_NONE; mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); @@ -1161,8 +1241,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { eq(false), eq(BiometricSourceType.FINGERPRINT)); // THEN locked out states are updated - assertThat(mKeyguardUpdateMonitor.isFingerprintLockedOut()).isEqualTo(fpLocked); - assertThat(mKeyguardUpdateMonitor.isFaceLockedOut()).isEqualTo(faceLocked); + assertThat(mKeyguardUpdateMonitor.isFingerprintLockedOut()).isEqualTo(fpLockOut); + assertThat(mKeyguardUpdateMonitor.isFaceLockedOut()).isEqualTo(faceLockOut); // Fingerprint should be cancelled on lockout if going to lockout state, else // restarted if it's not @@ -1443,7 +1523,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { throws RemoteException { // GIVEN SFPS supported and enrolled final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + props.add(createFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON, + /* isClass3 */ true)); when(mAuthController.getSfpsProps()).thenReturn(props); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); @@ -1466,17 +1547,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); } - private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( - @FingerprintSensorProperties.SensorType int sensorType) { - return new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, - SensorProperties.STRENGTH_STRONG, - 1 /* maxEnrollmentsPerUser */, - new ArrayList<ComponentInfoInternal>(), - sensorType, - true /* resetLockoutRequiresHardwareAuthToken */); - } - @Test public void testShouldNotListenForUdfps_whenTrustEnabled() { // GIVEN a "we should listen for udfps" state @@ -1613,7 +1683,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { keyguardNotGoingAway(); occludingAppRequestsFaceAuth(); currentUserIsPrimary(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1622,7 +1692,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); // Fingerprint is locked out. - fingerprintErrorTemporaryLockedOut(); + fingerprintErrorTemporaryLockOut(); assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); } @@ -1634,7 +1704,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { bouncerFullyVisibleAndNotGoingToSleep(); keyguardNotGoingAway(); currentUserIsPrimary(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1657,7 +1727,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { bouncerFullyVisibleAndNotGoingToSleep(); keyguardNotGoingAway(); currentUserIsPrimary(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1684,7 +1754,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // Face auth should run when the following is true. keyguardNotGoingAway(); bouncerFullyVisibleAndNotGoingToSleep(); - strongAuthNotRequired(); + primaryAuthNotRequiredByStrongAuthTracker(); biometricsEnabledForCurrentUser(); currentUserDoesNotHaveTrust(); biometricsNotDisabledThroughDevicePolicyManager(); @@ -1940,7 +2010,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); // Face is locked out. - faceAuthLockedOut(); + faceAuthLockOut(); mTestableLooper.processAllMessages(); // This is needed beccause we want to show face locked out error message whenever face auth @@ -2578,7 +2648,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mFingerprintManager).addLockoutResetCallback(fpLockoutResetCallbackCaptor.capture()); // GIVEN device is locked out - fingerprintErrorTemporaryLockedOut(); + fingerprintErrorTemporaryLockOut(); // GIVEN FP detection is running givenDetectFingerprintWithClearingFingerprintManagerInvocations(); @@ -2662,6 +2732,21 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { KeyguardUpdateMonitor.getCurrentUser())).isTrue(); } + @Test + public void testFingerprintListeningStateWhenOccluded() { + when(mAuthController.isUdfpsSupported()).thenReturn(true); + + mKeyguardUpdateMonitor.setKeyguardShowing(false, false); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_BIOMETRIC); + mKeyguardUpdateMonitor.setKeyguardShowing(false, true); + + verifyFingerprintAuthenticateNeverCalled(); + + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); + mKeyguardUpdateMonitor.setAlternateBouncerShowing(true); + + verifyFingerprintAuthenticateCall(); + } private void verifyFingerprintAuthenticateNeverCalled() { verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any()); @@ -2705,8 +2790,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void supportsFaceDetection() throws RemoteException { + final boolean isClass3 = !mFaceSensorProperties.isEmpty() + && mFaceSensorProperties.get(0).sensorStrength == STRENGTH_STRONG; mFaceSensorProperties = - List.of(createFaceSensorProperties(/* supportsFaceDetection = */ true)); + List.of(createFaceSensorProperties(/* supportsFaceDetection = */ true, isClass3)); mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); } @@ -2734,7 +2821,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } } - private void faceAuthLockedOut() { + private void faceAuthLockOut() { mKeyguardUpdateMonitor.mFaceAuthenticationCallback .onAuthenticationError(FaceManager.FACE_ERROR_LOCKOUT_PERMANENT, ""); } @@ -2767,7 +2854,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.setSwitchingUser(true); } - private void fingerprintErrorTemporaryLockedOut() { + private void fingerprintErrorTemporaryLockOut() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT, "Fingerprint locked out"); } @@ -2821,18 +2908,18 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { ); } - private void strongAuthRequiredEncrypted() { + private void primaryAuthRequiredEncrypted() { when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser())) .thenReturn(STRONG_AUTH_REQUIRED_AFTER_BOOT); when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); } - private void strongAuthRequiredForWeakBiometricOnly() { + private void primaryAuthRequiredForWeakBiometricOnly() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(eq(true))).thenReturn(true); when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(eq(false))).thenReturn(false); } - private void strongAuthNotRequired() { + private void primaryAuthNotRequiredByStrongAuthTracker() { when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser())) .thenReturn(0); when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); @@ -2917,10 +3004,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void givenDetectFace() throws RemoteException { - // GIVEN bypass is enabled, face detection is supported and strong auth is required + // GIVEN bypass is enabled, face detection is supported and primary auth is required lockscreenBypassIsAllowed(); supportsFaceDetection(); - strongAuthRequiredEncrypted(); + primaryAuthRequiredEncrypted(); keyguardIsVisible(); // fingerprint is NOT running, UDFPS is NOT supported diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/SplitShadeTransitionAdapterTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt index 64fec5bfd4ed..dea2082a2c22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/SplitShadeTransitionAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/SplitShadeTransitionAdapterTest.kt @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.shade +package com.android.keyguard import android.animation.Animator import android.testing.AndroidTestingRunner import android.transition.TransitionValues import androidx.test.filters.SmallTest -import com.android.keyguard.KeyguardStatusViewController +import com.android.keyguard.KeyguardStatusViewController.SplitShadeTransitionAdapter import com.android.systemui.SysuiTestCase -import com.android.systemui.shade.NotificationPanelViewController.SplitShadeTransitionAdapter import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -33,14 +32,14 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) class SplitShadeTransitionAdapterTest : SysuiTestCase() { - @Mock private lateinit var keyguardStatusViewController: KeyguardStatusViewController + @Mock private lateinit var KeyguardClockSwitchController: KeyguardClockSwitchController private lateinit var adapter: SplitShadeTransitionAdapter @Before fun setUp() { MockitoAnnotations.initMocks(this) - adapter = SplitShadeTransitionAdapter(keyguardStatusViewController) + adapter = SplitShadeTransitionAdapter(KeyguardClockSwitchController) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt deleted file mode 100644 index 44da5f42aafd..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/ChooserPinMigrationTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui - -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.res.Resources -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.broadcast.BroadcastSender -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.kotlinArgumentCaptor -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.ArgumentMatchers.anyInt -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -class ChooserPinMigrationTest : SysuiTestCase() { - - private val fakeFeatureFlags = FakeFeatureFlags() - private val fakePreferences = - mutableMapOf( - "TestPinnedPackage/TestPinnedClass" to true, - "TestUnpinnedPackage/TestUnpinnedClass" to false, - ) - private val intent = kotlinArgumentCaptor<Intent>() - private val permission = kotlinArgumentCaptor<String>() - - private lateinit var chooserPinMigration: ChooserPinMigration - - @Mock private lateinit var mockContext: Context - @Mock private lateinit var mockResources: Resources - @Mock - private lateinit var mockLegacyPinPrefsFileSupplier: - ChooserPinMigration.Companion.LegacyPinPrefsFileSupplier - @Mock private lateinit var mockFile: File - @Mock private lateinit var mockSharedPreferences: SharedPreferences - @Mock private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor - @Mock private lateinit var mockBroadcastSender: BroadcastSender - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - - whenever(mockContext.resources).thenReturn(mockResources) - whenever(mockContext.getSharedPreferences(any<File>(), anyInt())) - .thenReturn(mockSharedPreferences) - whenever(mockResources.getString(anyInt())).thenReturn("TestPackage/TestClass") - whenever(mockSharedPreferences.all).thenReturn(fakePreferences) - whenever(mockSharedPreferences.edit()).thenReturn(mockSharedPreferencesEditor) - whenever(mockSharedPreferencesEditor.commit()).thenReturn(true) - whenever(mockLegacyPinPrefsFileSupplier.get()).thenReturn(mockFile) - whenever(mockFile.exists()).thenReturn(true) - whenever(mockFile.delete()).thenReturn(true) - fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, true) - } - - @Test - fun start_performsMigration() { - // Arrange - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) - assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") - assertThat(intent.value.`package`).isEqualTo("TestPackage") - assertThat(intent.value.extras?.keySet()).hasSize(2) - assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) - .isTrue() - assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) - .isFalse() - assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") - - // Assert - verify(mockSharedPreferencesEditor).clear() - verify(mockSharedPreferencesEditor).commit() - - // Assert - verify(mockFile).delete() - } - - @Test - fun start_doesNotDeleteLegacyPreferencesFile_whenClearingItFails() { - // Arrange - whenever(mockSharedPreferencesEditor.commit()).thenReturn(false) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verify(mockBroadcastSender).sendBroadcast(intent.capture(), permission.capture()) - assertThat(intent.value.action).isEqualTo("android.intent.action.CHOOSER_PIN_MIGRATION") - assertThat(intent.value.`package`).isEqualTo("TestPackage") - assertThat(intent.value.extras?.keySet()).hasSize(2) - assertThat(intent.value.hasExtra("TestPinnedPackage/TestPinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestPinnedPackage/TestPinnedClass", false)) - .isTrue() - assertThat(intent.value.hasExtra("TestUnpinnedPackage/TestUnpinnedClass")).isTrue() - assertThat(intent.value.getBooleanExtra("TestUnpinnedPackage/TestUnpinnedClass", true)) - .isFalse() - assertThat(permission.value).isEqualTo("android.permission.RECEIVE_CHOOSER_PIN_MIGRATION") - - // Assert - verify(mockSharedPreferencesEditor).clear() - verify(mockSharedPreferencesEditor).commit() - - // Assert - verify(mockFile, never()).delete() - } - - @Test - fun start_OnlyDeletesLegacyPreferencesFile_whenEmpty() { - // Arrange - whenever(mockSharedPreferences.all).thenReturn(emptyMap()) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile).delete() - } - - @Test - fun start_DoesNotDoMigration_whenFlagIsDisabled() { - // Arrange - fakeFeatureFlags.set(Flags.CHOOSER_MIGRATION_ENABLED, false) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile, never()).delete() - } - - @Test - fun start_DoesNotDoMigration_whenLegacyPreferenceFileNotPresent() { - // Arrange - whenever(mockFile.exists()).thenReturn(false) - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile, never()).delete() - } - - @Test - fun start_DoesNotDoMigration_whenConfiguredChooserComponentIsInvalid() { - // Arrange - whenever(mockResources.getString(anyInt())).thenReturn("InvalidComponent") - chooserPinMigration = - ChooserPinMigration( - mockContext, - fakeFeatureFlags, - mockBroadcastSender, - mockLegacyPinPrefsFileSupplier, - ) - - // Act - chooserPinMigration.start() - - // Assert - verifyZeroInteractions(mockBroadcastSender) - - // Assert - verifyZeroInteractions(mockSharedPreferencesEditor) - - // Assert - verify(mockFile, never()).delete() - } -} 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 edee3f1b9f02..64c028e6a095 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -1017,7 +1017,7 @@ public class UdfpsControllerTest extends SysuiTestCase { // THEN the display should be unconfigured once. If the timeout action is not // cancelled, the display would be unconfigured twice which would cause two // FP attempts. - verify(mUdfpsView, times(1)).unconfigureDisplay(); + verify(mUdfpsView).unconfigureDisplay(); } else { verify(mUdfpsView, never()).unconfigureDisplay(); } @@ -1301,8 +1301,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mBiometricExecutor.runAllReady(); downEvent.recycle(); - // THEN the touch is pilfered, expected twice (valid overlap and touch on sensor) - verify(mInputManager, times(2)).pilferPointers(any()); + // THEN the touch is pilfered + verify(mInputManager).pilferPointers(any()); } @Test @@ -1340,7 +1340,7 @@ public class UdfpsControllerTest extends SysuiTestCase { downEvent.recycle(); // THEN the touch is NOT pilfered - verify(mInputManager, times(0)).pilferPointers(any()); + verify(mInputManager, never()).pilferPointers(any()); } @Test @@ -1380,7 +1380,51 @@ public class UdfpsControllerTest extends SysuiTestCase { downEvent.recycle(); // THEN the touch is pilfered - verify(mInputManager, times(1)).pilferPointers(any()); + verify(mInputManager).pilferPointers(any()); + } + + @Test + public void onTouch_withNewTouchDetection_doNotPilferWhenPullingUpBouncer() + throws RemoteException { + final NormalizedTouchData touchData = new NormalizedTouchData(0, 0f, 0f, 0f, 0f, 0f, 0L, + 0L); + final TouchProcessorResult processorResultMove = + new TouchProcessorResult.ProcessedTouch(InteractionEvent.DOWN, + 1 /* pointerId */, touchData); + + // Enable new touch detection. + when(mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)).thenReturn(true); + + // Configure UdfpsController to use FingerprintManager as opposed to AlternateTouchProvider. + initUdfpsController(mOpticalProps, false /* hasAlternateTouchProvider */); + + // Configure UdfpsView to accept the ACTION_MOVE event + when(mUdfpsView.isDisplayConfigured()).thenReturn(false); + when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); + + // GIVEN that the alternate bouncer is not showing and a11y touch exploration NOT enabled + when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false); + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); + mFgExecutor.runAllReady(); + + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + + // GIVEN a swipe up to bring up primary bouncer is in progress or swipe down for QS + when(mPrimaryBouncerInteractor.isInTransit()).thenReturn(true); + when(mLockscreenShadeTransitionController.getFractionToShade()).thenReturn(1f); + + // WHEN ACTION_MOVE is received and touch is within sensor + when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( + processorResultMove); + MotionEvent moveEvent = MotionEvent.obtain(0, 0, ACTION_MOVE, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); + mBiometricExecutor.runAllReady(); + moveEvent.recycle(); + + // THEN the touch is NOT pilfered + verify(mInputManager, never()).pilferPointers(any()); } @Test 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 8cb91304808d..4cb99a23a531 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -120,7 +120,6 @@ public class BrightLineClassifierTest extends SysuiTestCase { gestureCompleteListenerCaptor.capture()); 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); } @@ -260,13 +259,6 @@ public class BrightLineClassifierTest extends SysuiTestCase { } @Test - public void testIsFalseLongTap_FalseLongTap_NotFlagged() { - mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, false); - when(mLongTapClassifier.isTap(mMotionEventList, 0)).thenReturn(mFalsedResult); - assertThat(mBrightLineFalsingManager.isFalseLongTap(NO_PENALTY)).isFalse(); - } - - @Test public void testIsFalseLongTap_FalseLongTap() { when(mLongTapClassifier.isTap(mMotionEventList, 0)).thenReturn(mFalsedResult); assertThat(mBrightLineFalsingManager.isFalseLongTap(NO_PENALTY)).isTrue(); 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 315774aad71a..292fdff0027d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java @@ -94,7 +94,6 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { 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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java index 5fcf414f0251..8fdc4918b9fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java @@ -18,8 +18,8 @@ package com.android.systemui.dreams.complication; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -29,23 +29,45 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; +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.Mockito; +import org.mockito.MockitoAnnotations; import java.util.Collection; import java.util.HashSet; @SmallTest @RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class ComplicationCollectionLiveDataTest extends SysuiTestCase { + + private FakeExecutor mExecutor; + private DreamOverlayStateController mStateController; + private ComplicationCollectionLiveData mLiveData; + private FakeFeatureFlags mFeatureFlags; + @Mock + private Observer mObserver; + @Before - public void setUp() throws Exception { - allowTestableLooperAsMainThread(); + public void setUp() { + MockitoAnnotations.initMocks(this); + mFeatureFlags = new FakeFeatureFlags(); + mExecutor = new FakeExecutor(new FakeSystemClock()); + mFeatureFlags.set(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS, true); + mStateController = new DreamOverlayStateController( + mExecutor, + /* overlayEnabled= */ true, + mFeatureFlags); + mLiveData = new ComplicationCollectionLiveData(mStateController); } @Test @@ -53,45 +75,41 @@ public class ComplicationCollectionLiveDataTest extends SysuiTestCase { * Ensures registration and callback lifecycles are respected. */ public void testLifecycle() { - getContext().getMainExecutor().execute(() -> { - final DreamOverlayStateController stateController = - Mockito.mock(DreamOverlayStateController.class); - final ComplicationCollectionLiveData liveData = - new ComplicationCollectionLiveData(stateController); - final HashSet<Complication> complications = new HashSet<>(); - final Observer<Collection<Complication>> observer = Mockito.mock(Observer.class); - complications.add(Mockito.mock(Complication.class)); - - when(stateController.getComplications()).thenReturn(complications); - - liveData.observeForever(observer); - ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor = - ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); - - verify(stateController).addCallback(callbackCaptor.capture()); - verifyUpdate(observer, complications); - - complications.add(Mockito.mock(Complication.class)); - callbackCaptor.getValue().onComplicationsChanged(); - - verifyUpdate(observer, complications); - - callbackCaptor.getValue().onAvailableComplicationTypesChanged(); - - verifyUpdate(observer, complications); - }); + final HashSet<Complication> complications = new HashSet<>(); + mLiveData.observeForever(mObserver); + mExecutor.runAllReady(); + // Verify observer called with empty complications + assertObserverCalledWith(complications); + + addComplication(mock(Complication.class), complications); + assertObserverCalledWith(complications); + + addComplication(mock(Complication.class), complications); + assertObserverCalledWith(complications); + + mStateController.setAvailableComplicationTypes(0); + mExecutor.runAllReady(); + assertObserverCalledWith(complications); + mLiveData.removeObserver(mObserver); } - void verifyUpdate(Observer<Collection<Complication>> observer, - Collection<Complication> targetCollection) { + private void assertObserverCalledWith(Collection<Complication> targetCollection) { ArgumentCaptor<Collection<Complication>> collectionCaptor = ArgumentCaptor.forClass(Collection.class); - verify(observer).onChanged(collectionCaptor.capture()); + verify(mObserver).onChanged(collectionCaptor.capture()); + + final Collection<Complication> collection = collectionCaptor.getValue(); - final Collection collection = collectionCaptor.getValue(); assertThat(collection.containsAll(targetCollection) && targetCollection.containsAll(collection)).isTrue(); - Mockito.clearInvocations(observer); + Mockito.clearInvocations(mObserver); + } + + private void addComplication(Complication complication, + Collection<Complication> complications) { + complications.add(complication); + mStateController.addComplication(complication); + mExecutor.runAllReady(); } } 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 c93e677071cd..0de9608b906f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -66,6 +66,8 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; @@ -142,6 +144,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock CentralSurfaces mCentralSurfaces; + private FakeFeatureFlags mFeatureFlags; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -160,6 +164,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mColorExtractor, mDumpManager, mKeyguardStateController, mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager, mShadeWindowLogger); + mFeatureFlags = new FakeFeatureFlags(); + DejankUtils.setImmediate(true); @@ -515,6 +521,28 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); } + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testNotStartingKeyguardWhenFlagIsDisabled() { + mViewMediator.setShowingLocked(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); + + mFeatureFlags.set(Flags.LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING, false); + mViewMediator.onDreamingStarted(); + assertFalse(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testStartingKeyguardWhenFlagIsEnabled() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + + mFeatureFlags.set(Flags.LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING, true); + mViewMediator.onDreamingStarted(); + assertTrue(mViewMediator.isShowingAndNotOccluded()); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, @@ -545,7 +573,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mShadeController, () -> mNotificationShadeWindowController, () -> mActivityLaunchAnimator, - () -> mScrimController); + () -> mScrimController, + mFeatureFlags); mViewMediator.start(); mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null); 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 86e8c9accc45..a668af340e7b 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 @@ -41,6 +41,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 java.util.Locale import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -67,6 +68,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { @Before fun setUp() { + context.resources.configuration.setLayoutDirection(Locale.US) config1 = FakeKeyguardQuickAffordanceConfig(FakeCustomizationProviderClient.AFFORDANCE_1) config2 = FakeKeyguardQuickAffordanceConfig(FakeCustomizationProviderClient.AFFORDANCE_2) val testDispatcher = StandardTestDispatcher() @@ -222,6 +224,40 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { } @Test + fun getSlotPickerRepresentations_rightToLeft_slotsReversed() { + context.resources.configuration.setLayoutDirection(Locale("he", "IL")) + val slot1 = "slot1" + val slot2 = "slot2" + val slot3 = "slot3" + context.orCreateTestableResources.addOverride( + R.array.config_keyguardQuickAffordanceSlots, + arrayOf( + "$slot1:2", + "$slot2:4", + "$slot3:5", + ), + ) + + assertThat(underTest.getSlotPickerRepresentations()) + .isEqualTo( + listOf( + KeyguardSlotPickerRepresentation( + id = slot3, + maxSelectedAffordances = 5, + ), + KeyguardSlotPickerRepresentation( + id = slot2, + maxSelectedAffordances = 4, + ), + KeyguardSlotPickerRepresentation( + id = slot1, + maxSelectedAffordances = 2, + ), + ) + ) + } + + @Test fun `selections for secondary user`() = testScope.runTest { userTracker.set( 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 index 51988ef1ab78..77bb12c2cbda 100644 --- 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 @@ -29,20 +29,20 @@ 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.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi 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 org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -51,8 +51,8 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class KeyguardLongPressInteractorTest : SysuiTestCase() { - @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var logger: UiEventLogger + @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper private lateinit var underTest: KeyguardLongPressInteractor @@ -63,6 +63,14 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt())).thenAnswer { + it.arguments[0] + } + + testScope = TestScope() + keyguardRepository = FakeKeyguardRepository() + keyguardTransitionRepository = FakeKeyguardTransitionRepository() + runBlocking { createUnderTest() } } @@ -98,60 +106,117 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { } @Test - fun `long-pressed - pop-up clicked - starts activity`() = + fun longPressed_menuClicked_showsSettings() = testScope.runTest { - val menu = collectLastValue(underTest.menu) + val isMenuVisible by collectLastValue(underTest.isMenuVisible) + val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings) 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) + underTest.onLongPress() + assertThat(isMenuVisible).isTrue() + + underTest.onMenuTouchGestureEnded(/* isClick= */ true) + + assertThat(isMenuVisible).isFalse() + assertThat(shouldOpenSettings).isTrue() + } + + @Test + fun onSettingsShown_consumesSettingsShowEvent() = + testScope.runTest { + val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings) + runCurrent() - menu()?.onClicked?.invoke() + underTest.onLongPress() + underTest.onMenuTouchGestureEnded(/* isClick= */ true) + assertThat(shouldOpenSettings).isTrue() - assertThat(menu()).isNull() - verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean()) + underTest.onSettingsShown() + assertThat(shouldOpenSettings).isFalse() } @Test - fun `long-pressed - pop-up dismissed - never starts activity`() = + fun onTouchedOutside_neverShowsSettings() = testScope.runTest { - val menu = collectLastValue(underTest.menu) + val isMenuVisible by collectLastValue(underTest.isMenuVisible) + val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings) runCurrent() - menu()?.onDismissed?.invoke() + underTest.onTouchedOutside() - assertThat(menu()).isNull() - verify(activityStarter, never()).dismissKeyguardThenExecute(any(), any(), anyBoolean()) + assertThat(isMenuVisible).isFalse() + assertThat(shouldOpenSettings).isFalse() + } + + @Test + fun longPressed_openWppDirectlyEnabled_doesNotShowMenu_opensSettings() = + testScope.runTest { + createUnderTest(isOpenWppDirectlyEnabled = true) + val isMenuVisible by collectLastValue(underTest.isMenuVisible) + val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings) + runCurrent() + + underTest.onLongPress() + + assertThat(isMenuVisible).isFalse() + assertThat(shouldOpenSettings).isTrue() } - @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) + val isMenuVisible by collectLastValue(underTest.isMenuVisible) runCurrent() - underTest.onLongPress(123, 456) - assertThat(menu()).isNotNull() + underTest.onLongPress() + assertThat(isMenuVisible).isTrue() fakeBroadcastDispatcher.registeredReceivers.forEach { broadcastReceiver -> broadcastReceiver.onReceive(context, Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) } - assertThat(menu()).isNull() + assertThat(isMenuVisible).isFalse() + } + + @Test + fun closesDialogAfterTimeout() = + testScope.runTest { + val isMenuVisible by collectLastValue(underTest.isMenuVisible) + runCurrent() + + underTest.onLongPress() + assertThat(isMenuVisible).isTrue() + + advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS) + + assertThat(isMenuVisible).isFalse() + } + + @Test + fun closesDialogAfterTimeout_onlyAfterTouchGestureEnded() = + testScope.runTest { + val isMenuVisible by collectLastValue(underTest.isMenuVisible) + runCurrent() + + underTest.onLongPress() + assertThat(isMenuVisible).isTrue() + underTest.onMenuTouchGestureStarted() + + advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS) + assertThat(isMenuVisible).isTrue() + + underTest.onMenuTouchGestureEnded(/* isClick= */ false) + advanceTimeBy(KeyguardLongPressInteractor.DEFAULT_POPUP_AUTO_HIDE_TIMEOUT_MS) + assertThat(isMenuVisible).isFalse() } @Test fun `logs when menu is shown`() = testScope.runTest { - collectLastValue(underTest.menu) + collectLastValue(underTest.isMenuVisible) runCurrent() - underTest.onLongPress(100, 123) + underTest.onLongPress() verify(logger) .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_SHOWN) @@ -160,41 +225,61 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { @Test fun `logs when menu is clicked`() = testScope.runTest { - val menu = collectLastValue(underTest.menu) + collectLastValue(underTest.isMenuVisible) runCurrent() - underTest.onLongPress(100, 123) - menu()?.onClicked?.invoke() + underTest.onLongPress() + underTest.onMenuTouchGestureEnded(/* isClick= */ true) verify(logger) .log(KeyguardLongPressInteractor.LogEvents.LOCK_SCREEN_LONG_PRESS_POPUP_CLICKED) } + @Test + fun showMenu_leaveLockscreen_returnToLockscreen_menuNotVisible() = + testScope.runTest { + val isMenuVisible by collectLastValue(underTest.isMenuVisible) + runCurrent() + underTest.onLongPress() + assertThat(isMenuVisible).isTrue() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + to = KeyguardState.GONE, + ), + ) + assertThat(isMenuVisible).isFalse() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + to = KeyguardState.LOCKSCREEN, + ), + ) + assertThat(isMenuVisible).isFalse() + } + private suspend fun createUnderTest( isLongPressFeatureEnabled: Boolean = true, isRevampedWppFeatureEnabled: Boolean = true, + isOpenWppDirectlyEnabled: Boolean = false, ) { - 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) + set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled) }, broadcastDispatcher = fakeBroadcastDispatcher, + accessibilityManager = accessibilityManager ) setUpState() } 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 bfc09d7c0379..224eec1bf3ac 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 @@ -20,10 +20,12 @@ import android.app.admin.DevicePolicyManager import android.content.Intent import android.os.UserHandle import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.animation.Expandable +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.util.BurnInHelperWrapper @@ -38,10 +40,13 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardLongPressInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition @@ -51,6 +56,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.AccessibilityManagerWrapper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any @@ -91,6 +97,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var accessibilityManager: AccessibilityManagerWrapper private lateinit var underTest: KeyguardBottomAreaViewModel @@ -134,6 +142,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { FakeFeatureFlags().apply { set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) set(Flags.FACE_AUTH_REFACTOR, true) + set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) + set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) } val keyguardInteractor = @@ -196,6 +206,19 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { dumpManager = mock(), userHandle = UserHandle.SYSTEM, ) + val keyguardLongPressInteractor = + KeyguardLongPressInteractor( + scope = testScope.backgroundScope, + transitionInteractor = + KeyguardTransitionInteractor( + repository = FakeKeyguardTransitionRepository(), + ), + repository = repository, + logger = UiEventLoggerFake(), + featureFlags = featureFlags, + broadcastDispatcher = broadcastDispatcher, + accessibilityManager = accessibilityManager, + ) underTest = KeyguardBottomAreaViewModel( keyguardInteractor = keyguardInteractor, @@ -216,6 +239,14 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { ), bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), burnInHelperWrapper = burnInHelperWrapper, + longPressViewModel = + KeyguardLongPressViewModel( + interactor = keyguardLongPressInteractor, + ), + settingsMenuViewModel = + KeyguardSettingsMenuViewModel( + interactor = keyguardLongPressInteractor, + ), ) } 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 543875dc31cf..aace5661862b 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 @@ -101,6 +101,7 @@ import dagger.Lazy import junit.framework.Assert.assertTrue import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -2199,6 +2200,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + @Ignore("b/276920368") fun bindRecommendation_carouselNotFitThreeRecs() { useRealConstraintSets() setupUpdatedRecommendationViewHolder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 34d2b14d46a9..aa92177e863e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -51,8 +51,10 @@ import com.android.internal.util.CollectionUtils; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.nano.SystemUIProtoDump; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.qs.QSFactory; @@ -62,7 +64,6 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.CustomTileStatePersister; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; -import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.UserFileManager; @@ -110,8 +111,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private Provider<AutoTileManager> mAutoTiles; @Mock - private DumpManager mDumpManager; - @Mock private CentralSurfaces mCentralSurfaces; @Mock private QSLogger mQSLogger; @@ -125,10 +124,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private CustomTileStatePersister mCustomTileStatePersister; @Mock - private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder; - @Mock - private TileServiceRequestController mTileServiceRequestController; - @Mock private TileLifecycleManager.Factory mTileLifecycleManagerFactory; @Mock private TileLifecycleManager mTileLifecycleManager; @@ -137,6 +132,8 @@ public class QSTileHostTest extends SysuiTestCase { private SparseArray<SharedPreferences> mSharedPreferencesByUser; + private FakeFeatureFlags mFeatureFlags; + private FakeExecutor mMainExecutor; private QSTileHost mQSTileHost; @@ -144,12 +141,13 @@ public class QSTileHostTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + mFeatureFlags = new FakeFeatureFlags(); + + mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false); + mMainExecutor = new FakeExecutor(new FakeSystemClock()); mSharedPreferencesByUser = new SparseArray<>(); - - when(mTileServiceRequestControllerBuilder.create(any())) - .thenReturn(mTileServiceRequestController); when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class))) .thenReturn(mTileLifecycleManager); when(mUserFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) @@ -165,10 +163,9 @@ public class QSTileHostTest extends SysuiTestCase { mSecureSettings = new FakeSettings(); saveSetting(""); mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor, - mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces, + mPluginManager, mTunerService, mAutoTiles, mCentralSurfaces, mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister, - mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory, - mUserFileManager); + mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags); mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) { @Override @@ -686,18 +683,16 @@ public class QSTileHostTest extends SysuiTestCase { TestQSTileHost(Context context, QSFactory defaultFactory, Executor mainExecutor, PluginManager pluginManager, TunerService tunerService, - Provider<AutoTileManager> autoTiles, DumpManager dumpManager, + Provider<AutoTileManager> autoTiles, CentralSurfaces centralSurfaces, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, - TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory, - UserFileManager userFileManager) { + UserFileManager userFileManager, FeatureFlags featureFlags) { super(context, defaultFactory, mainExecutor, pluginManager, - tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger, + tunerService, autoTiles, Optional.of(centralSurfaces), qsLogger, uiEventLogger, userTracker, secureSettings, customTileStatePersister, - tileServiceRequestControllerBuilder, tileLifecycleManagerFactory, - userFileManager); + tileLifecycleManagerFactory, userFileManager, featureFlags); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index c03849b35f54..50a8d2630d79 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -170,6 +170,21 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { } @Test + fun addTileAtPosition_tooLarge_addedAtEnd() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + storeTilesForUser(specs, 0) + + underTest.addTile(userId = 0, TileSpec.create("d"), position = 100) + + val expected = "a,custom(b/c),d" + assertThat(loadTilesForUser(0)).isEqualTo(expected) + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + } + + @Test fun addTileForOtherUser_addedInThatUser() = testScope.runTest { val tilesUser0 by collectLastValue(underTest.tilesSpecs(0)) @@ -187,27 +202,27 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { } @Test - fun removeTile() = + fun removeTiles() = testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) storeTilesForUser("a,b", 0) - underTest.removeTile(userId = 0, TileSpec.create("a")) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"))) assertThat(loadTilesForUser(0)).isEqualTo("b") assertThat(tiles).isEqualTo("b".toTileSpecs()) } @Test - fun removeTileNotThere_noop() = + fun removeTilesNotThere_noop() = testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) val specs = "a,b" storeTilesForUser(specs, 0) - underTest.removeTile(userId = 0, TileSpec.create("c")) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("c"))) assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(tiles).isEqualTo(specs.toTileSpecs()) @@ -221,7 +236,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { val specs = "a,b" storeTilesForUser(specs, 0) - underTest.removeTile(userId = 0, TileSpec.Invalid) + underTest.removeTiles(userId = 0, listOf(TileSpec.Invalid)) assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(tiles).isEqualTo(specs.toTileSpecs()) @@ -237,7 +252,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(specs, 0) storeTilesForUser(specs, 1) - underTest.removeTile(userId = 1, TileSpec.create("a")) + underTest.removeTiles(userId = 1, listOf(TileSpec.create("a"))) assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) @@ -246,6 +261,19 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { } @Test + fun removeMultipleTiles() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a,b,c,d", 0) + + underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"), TileSpec.create("c"))) + + assertThat(loadTilesForUser(0)).isEqualTo("b,d") + assertThat(tiles).isEqualTo("b,d".toTileSpecs()) + } + + @Test fun changeTiles() = testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) @@ -310,8 +338,8 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(specs, 0) coroutineScope { - underTest.removeTile(userId = 0, TileSpec.create("c")) - underTest.removeTile(userId = 0, TileSpec.create("a")) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("c"))) + underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"))) } assertThat(loadTilesForUser(0)).isEqualTo("b") diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt new file mode 100644 index 000000000000..7ecb4dc9376a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -0,0 +1,674 @@ +/* + * 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.qs.pipeline.domain.interactor + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.UserHandle +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTile.BooleanState +import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.external.CustomTileStatePersister +import com.android.systemui.qs.external.TileLifecycleManager +import com.android.systemui.qs.external.TileServiceKey +import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.FakeCustomTileAddedRepository +import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.domain.model.TileModel +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.qs.toProto +import com.android.systemui.settings.UserTracker +import com.android.systemui.user.data.repository.FakeUserRepository +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 com.google.protobuf.nano.MessageNano +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +class CurrentTilesInteractorImplTest : SysuiTestCase() { + + private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository() + private val userRepository = FakeUserRepository() + private val tileFactory = FakeQSFactory(::tileCreator) + private val customTileAddedRepository: CustomTileAddedRepository = + FakeCustomTileAddedRepository() + private val featureFlags = FakeFeatureFlags() + private val tileLifecycleManagerFactory = TLMFactory() + + @Mock private lateinit var customTileStatePersister: CustomTileStatePersister + + @Mock private lateinit var userTracker: UserTracker + + @Mock private lateinit var logger: QSPipelineLogger + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val unavailableTiles = mutableSetOf("e") + + private lateinit var underTest: CurrentTilesInteractorImpl + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true) + + userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1)) + setUserTracker(0) + + underTest = + CurrentTilesInteractorImpl( + tileSpecRepository = tileSpecRepository, + userRepository = userRepository, + customTileStatePersister = customTileStatePersister, + tileFactory = tileFactory, + customTileAddedRepository = customTileAddedRepository, + tileLifecycleManagerFactory = tileLifecycleManagerFactory, + userTracker = userTracker, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, + scope = testScope.backgroundScope, + logger = logger, + featureFlags = featureFlags, + ) + } + + @Test + fun initialState() = + testScope.runTest(USER_INFO_0) { + assertThat(underTest.currentTiles.value).isEmpty() + assertThat(underTest.currentQSTiles).isEmpty() + assertThat(underTest.currentTilesSpecs).isEmpty() + assertThat(underTest.userId.value).isEqualTo(0) + assertThat(underTest.userContext.value.userId).isEqualTo(0) + } + + @Test + fun correctTiles() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = + listOf( + TileSpec.create("a"), + TileSpec.create("e"), + CUSTOM_TILE_SPEC, + TileSpec.create("d"), + TileSpec.create("non_existent") + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + // check each tile + + // Tile a + val tile0 = tiles!![0] + assertThat(tile0.spec).isEqualTo(specs[0]) + assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec) + assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java) + assertThat(tile0.tile.isAvailable).isTrue() + + // Tile e is not available and is not in the list + + // Custom Tile + val tile1 = tiles!![1] + assertThat(tile1.spec).isEqualTo(specs[2]) + assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec) + assertThat(tile1.tile).isInstanceOf(CustomTile::class.java) + assertThat(tile1.tile.isAvailable).isTrue() + + // Tile d + val tile2 = tiles!![2] + assertThat(tile2.spec).isEqualTo(specs[3]) + assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec) + assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java) + assertThat(tile2.tile.isAvailable).isTrue() + + // Tile non-existent shouldn't be created. Therefore, only 3 tiles total + assertThat(tiles?.size).isEqualTo(3) + } + + @Test + fun logTileCreated() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("a"), + CUSTOM_TILE_SPEC, + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + specs.forEach { verify(logger).logTileCreated(it) } + } + + @Test + fun logTileNotFoundInFactory() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("non_existing"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + verify(logger, never()).logTileCreated(any()) + verify(logger).logTileNotFoundInFactory(specs[0]) + } + + @Test + fun tileNotAvailableDestroyed_logged() = + testScope.runTest(USER_INFO_0) { + val specs = + listOf( + TileSpec.create("e"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + verify(logger, never()).logTileCreated(any()) + verify(logger) + .logTileDestroyed( + specs[0], + QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE + ) + } + + @Test + fun someTilesNotValid_repositorySetToDefinitiveList() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = + listOf( + TileSpec.create("a"), + TileSpec.create("e"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + } + + @Test + fun deduplicatedTiles() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(TileSpec.create("a"), TileSpec.create("a")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(specs[0]) + } + + @Test + fun tilesChange_platformTileNotRecreated() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = + listOf( + TileSpec.create("a"), + ) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileA = tiles!![0].tile + + tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) + + assertThat(tiles?.size).isEqualTo(2) + assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + } + + @Test + fun tileRemovedIsDestroyed() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(TileSpec.create("a"), TileSpec.create("c")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileC = tiles!![1].tile + + tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c"))) + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a")) + + assertThat((originalTileC as FakeQSTile).destroyed).isTrue() + verify(logger) + .logTileDestroyed( + TileSpec.create("c"), + QSPipelineLogger.TileDestroyedReason.TILE_REMOVED + ) + } + + @Test + fun tileBecomesNotAvailable_destroyed() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a")) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + val originalTileA = tiles!![0].tile + + // Tile becomes unavailable + (originalTileA as FakeQSTile).available = false + unavailableTiles.add("a") + // and there is some change in the specs + tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b")) + runCurrent() + + assertThat(originalTileA.destroyed).isTrue() + verify(logger) + .logTileDestroyed( + TileSpec.create("a"), + QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE + ) + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b")) + assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA) + + assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec)) + } + + @Test + fun userChange_tilesChange() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs0 = listOf(TileSpec.create("a")) + val specs1 = listOf(TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs0) + tileSpecRepository.setTiles(USER_INFO_1.id, specs1) + + switchUser(USER_INFO_1) + + assertThat(tiles!![0].spec).isEqualTo(specs1[0]) + assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec) + } + + @Test + fun tileNotPresentInSecondaryUser_destroyedInUserChange() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs0 = listOf(TileSpec.create("a")) + val specs1 = listOf(TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs0) + tileSpecRepository.setTiles(USER_INFO_1.id, specs1) + + val originalTileA = tiles!![0].tile + + switchUser(USER_INFO_1) + runCurrent() + + assertThat((originalTileA as FakeQSTile).destroyed).isTrue() + verify(logger) + .logTileDestroyed( + specs0[0], + QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER + ) + } + + @Test + fun userChange_customTileDestroyed_lifecycleNotTerminated() { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_1.id, specs) + + val originalCustomTile = tiles!![0].tile + + switchUser(USER_INFO_1) + runCurrent() + + verify(originalCustomTile).destroy() + assertThat(tileLifecycleManagerFactory.created).isEmpty() + } + } + + @Test + fun userChange_sameTileUserChanged() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + + val specs = listOf(TileSpec.create("a")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + tileSpecRepository.setTiles(USER_INFO_1.id, specs) + + val originalTileA = tiles!![0].tile as FakeQSTile + assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id) + + switchUser(USER_INFO_1) + runCurrent() + + assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA) + assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id) + verify(logger).logTileUserChanged(specs[0], USER_INFO_1.id) + } + + @Test + fun addTile() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val spec = TileSpec.create("a") + val currentSpecs = + listOf( + TileSpec.create("b"), + TileSpec.create("c"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + + underTest.addTile(spec, position = 1) + + val expectedSpecs = + listOf( + TileSpec.create("b"), + spec, + TileSpec.create("c"), + ) + assertThat(tiles).isEqualTo(expectedSpecs) + } + + @Test + fun addTile_currentUser() = + testScope.runTest(USER_INFO_1) { + val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) + val spec = TileSpec.create("a") + val currentSpecs = + listOf( + TileSpec.create("b"), + TileSpec.create("c"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) + + switchUser(USER_INFO_1) + underTest.addTile(spec, position = 1) + + assertThat(tiles0).isEqualTo(currentSpecs) + + val expectedSpecs = + listOf( + TileSpec.create("b"), + spec, + TileSpec.create("c"), + ) + assertThat(tiles1).isEqualTo(expectedSpecs) + } + + @Test + fun removeTile_platform() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a"), TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + + underTest.removeTiles(specs.subList(0, 1)) + + assertThat(tiles).isEqualTo(specs.subList(1, 2)) + } + + @Test + fun removeTile_customTile_lifecycleEnded() { + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + runCurrent() + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isTrue() + + underTest.removeTiles(listOf(CUSTOM_TILE_SPEC)) + + assertThat(tiles).isEqualTo(specs.subList(0, 1)) + + val tileLifecycleManager = + tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT] + assertThat(tileLifecycleManager).isNotNull() + + with(inOrder(tileLifecycleManager!!)) { + verify(tileLifecycleManager).onStopListening() + verify(tileLifecycleManager).onTileRemoved() + verify(tileLifecycleManager).flushMessagesAndUnbind() + } + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isFalse() + verify(customTileStatePersister) + .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)) + } + } + + @Test + fun removeTiles_currentUser() = + testScope.runTest { + val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id)) + val currentSpecs = + listOf( + TileSpec.create("a"), + TileSpec.create("b"), + TileSpec.create("c"), + ) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs) + + switchUser(USER_INFO_1) + runCurrent() + + underTest.removeTiles(currentSpecs.subList(0, 2)) + + assertThat(tiles0).isEqualTo(currentSpecs) + assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3)) + } + + @Test + fun setTiles() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id)) + + val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b")) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + runCurrent() + + val newSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a")) + underTest.setTiles(newSpecs) + runCurrent() + + assertThat(tiles).isEqualTo(newSpecs) + } + + @Test + fun setTiles_customTiles_lifecycleEndedIfGone() = + testScope.runTest(USER_INFO_0) { + val otherCustomTileSpec = TileSpec.create("custom(b/c)") + + val currentSpecs = listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec) + tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs) + runCurrent() + + val newSpecs = + listOf( + otherCustomTileSpec, + TileSpec.create("a"), + ) + + underTest.setTiles(newSpecs) + runCurrent() + + val tileLifecycleManager = + tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]!! + + with(inOrder(tileLifecycleManager)) { + verify(tileLifecycleManager).onStopListening() + verify(tileLifecycleManager).onTileRemoved() + verify(tileLifecycleManager).flushMessagesAndUnbind() + } + assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id)) + .isFalse() + verify(customTileStatePersister) + .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)) + } + + @Test + fun protoDump() = + testScope.runTest(USER_INFO_0) { + val tiles by collectLastValue(underTest.currentTiles) + val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC) + + tileSpecRepository.setTiles(USER_INFO_0.id, specs) + + val stateA = tiles!![0].tile.state + stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA") + val stateCustom = QSTile.BooleanState() + stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB") + stateCustom.spec = CUSTOM_TILE_SPEC.spec + whenever(tiles!![1].tile.state).thenReturn(stateCustom) + + val proto = SystemUIProtoDump() + underTest.dumpProto(proto, emptyArray()) + + assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue() + assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto())) + .isTrue() + } + + private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) { + this.state = state + this.label = label + this.secondaryLabel = secondaryLabel + if (this is BooleanState) { + value = state == Tile.STATE_ACTIVE + } + } + + private fun tileCreator(spec: String): QSTile? { + val currentUser = userTracker.userId + return when (spec) { + CUSTOM_TILE_SPEC.spec -> + mock<CustomTile> { + var tileSpecReference: String? = null + whenever(user).thenReturn(currentUser) + whenever(component).thenReturn(CUSTOM_TILE_SPEC.componentName) + whenever(isAvailable).thenReturn(true) + whenever(setTileSpec(anyString())).thenAnswer { + tileSpecReference = it.arguments[0] as? String + Unit + } + whenever(tileSpec).thenAnswer { tileSpecReference } + // Also, add it to the set of added tiles (as this happens as part of the tile + // creation). + customTileAddedRepository.setTileAdded( + CUSTOM_TILE_SPEC.componentName, + currentUser, + true + ) + } + in VALID_TILES -> FakeQSTile(currentUser, available = spec !in unavailableTiles) + else -> null + } + } + + private fun TestScope.runTest(user: UserInfo, body: suspend TestScope.() -> Unit) { + return runTest { + switchUser(user) + body() + } + } + + private suspend fun switchUser(user: UserInfo) { + setUserTracker(user.id) + userRepository.setSelectedUserInfo(user) + } + + private fun setUserTracker(user: Int) { + val mockContext = mockUserContext(user) + whenever(userTracker.userContext).thenReturn(mockContext) + whenever(userTracker.userId).thenReturn(user) + } + + private class TLMFactory : TileLifecycleManager.Factory { + + val created = mutableMapOf<Pair<Int, ComponentName>, TileLifecycleManager>() + + override fun create(intent: Intent, userHandle: UserHandle): TileLifecycleManager { + val componentName = intent.component!! + val user = userHandle.identifier + val manager: TileLifecycleManager = mock() + created[user to componentName] = manager + return manager + } + } + + private fun mockUserContext(user: Int): Context { + return mock { + whenever(this.userId).thenReturn(user) + whenever(this.user).thenReturn(UserHandle.of(user)) + } + } + + companion object { + private val USER_INFO_0 = UserInfo().apply { id = 0 } + private val USER_INFO_1 = UserInfo().apply { id = 1 } + + private val VALID_TILES = setOf("a", "b", "c", "d", "e") + private val TEST_COMPONENT = ComponentName("pkg", "cls") + private val CUSTOM_TILE_SPEC = TileSpec.Companion.create(TEST_COMPONENT) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt new file mode 100644 index 000000000000..e50969641a71 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.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.qs.pipeline.domain.interactor + +import android.content.Context +import android.view.View +import com.android.internal.logging.InstanceId +import com.android.systemui.plugins.qs.QSIconView +import com.android.systemui.plugins.qs.QSTile + +class FakeQSTile( + var user: Int, + var available: Boolean = true, +) : QSTile { + private var tileSpec: String? = null + var destroyed = false + private val state = QSTile.State() + + override fun getTileSpec(): String? { + return tileSpec + } + + override fun isAvailable(): Boolean { + return available + } + + override fun setTileSpec(tileSpec: String?) { + this.tileSpec = tileSpec + state.spec = tileSpec + } + + override fun refreshState() {} + + override fun addCallback(callback: QSTile.Callback?) {} + + override fun removeCallback(callback: QSTile.Callback?) {} + + override fun removeCallbacks() {} + + override fun createTileView(context: Context?): QSIconView? { + return null + } + + override fun click(view: View?) {} + + override fun secondaryClick(view: View?) {} + + override fun longClick(view: View?) {} + + override fun userSwitch(currentUser: Int) { + user = currentUser + } + + override fun getMetricsCategory(): Int { + return 0 + } + + override fun setListening(client: Any?, listening: Boolean) {} + + override fun setDetailListening(show: Boolean) {} + + override fun destroy() { + destroyed = true + } + + override fun getTileLabel(): CharSequence { + return "" + } + + override fun getState(): QSTile.State { + return state + } + + override fun getInstanceId(): InstanceId { + return InstanceId.fakeInstanceId(0) + } + + override fun isListening(): Boolean { + return false + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 7b37ea0c9a1a..5ca37716cbff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -66,6 +67,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardClockSwitch; import com.android.keyguard.KeyguardClockSwitchController; +import com.android.keyguard.KeyguardSliceViewController; import com.android.keyguard.KeyguardStatusView; import com.android.keyguard.KeyguardStatusViewController; import com.android.keyguard.KeyguardUpdateMonitor; @@ -74,6 +76,7 @@ import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent; import com.android.keyguard.dagger.KeyguardStatusBarViewComponent; import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.keyguard.dagger.KeyguardUserSwitcherComponent; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; @@ -106,6 +109,7 @@ import com.android.systemui.model.SysUiState; import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.qs.QSFragment; @@ -232,7 +236,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory; @Mock protected KeyguardStatusBarViewComponent mKeyguardStatusBarViewComponent; @Mock protected KeyguardClockSwitchController mKeyguardClockSwitchController; - @Mock protected KeyguardStatusViewController mKeyguardStatusViewController; @Mock protected KeyguardStatusBarViewController mKeyguardStatusBarViewController; @Mock protected NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; @@ -292,9 +295,13 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock protected MotionEvent mDownMotionEvent; @Mock protected CoroutineDispatcher mMainDispatcher; + @Mock protected KeyguardSliceViewController mKeyguardSliceViewController; + @Mock protected KeyguardLogger mKeyguardLogger; + @Mock protected KeyguardStatusView mKeyguardStatusView; @Captor protected ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> mEmptySpaceClickListenerCaptor; + @Mock protected ActivityStarter mActivityStarter; protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; protected KeyguardInteractor mKeyguardInteractor; @@ -307,6 +314,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners; protected Handler mMainHandler; protected View.OnLayoutChangeListener mLayoutChangeListener; + protected KeyguardStatusViewController mKeyguardStatusViewController; protected final FalsingManagerFake mFalsingManager = new FalsingManagerFake(); protected final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty(); @@ -333,6 +341,18 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext); keyguardStatusView.setId(R.id.keyguard_status_view); + mKeyguardStatusViewController = spy(new KeyguardStatusViewController( + mKeyguardStatusView, + mKeyguardSliceViewController, + mKeyguardClockSwitchController, + mKeyguardStateController, + mUpdateMonitor, + mConfigurationController, + mDozeParameters, + mScreenOffAnimationController, + mKeyguardLogger, + mFeatureFlags, + mInteractionJankMonitor)); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); when(mHeadsUpCallback.getContext()).thenReturn(mContext); @@ -364,12 +384,15 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mView.findViewById(R.id.keyguard_bottom_area)).thenReturn(mKeyguardBottomArea); when(mKeyguardBottomArea.animate()).thenReturn(mViewPropertyAnimator); when(mView.animate()).thenReturn(mViewPropertyAnimator); + when(mKeyguardStatusView.animate()).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.translationX(anyFloat())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.alpha(anyFloat())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setDuration(anyLong())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.setStartDelay(anyLong())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setInterpolator(any())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setListener(any())).thenReturn(mViewPropertyAnimator); when(mViewPropertyAnimator.setUpdateListener(any())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.withEndAction(any())).thenReturn(mViewPropertyAnimator); when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame); when(mView.findViewById(R.id.keyguard_status_view)) .thenReturn(mock(KeyguardStatusView.class)); @@ -575,7 +598,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { () -> mMultiShadeInteractor, mDumpManager, mKeyuardLongPressViewModel, - mKeyguardInteractor); + mKeyguardInteractor, + mActivityStarter); mNotificationPanelViewController.initDependencies( mCentralSurfaces, null, @@ -647,9 +671,13 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @After public void tearDown() { - mNotificationPanelViewController.mBottomAreaShadeAlphaAnimator.cancel(); - mNotificationPanelViewController.cancelHeightAnimator(); - mMainHandler.removeCallbacksAndMessages(null); + if (mNotificationPanelViewController != null) { + mNotificationPanelViewController.mBottomAreaShadeAlphaAnimator.cancel(); + mNotificationPanelViewController.cancelHeightAnimator(); + } + if (mMainHandler != null) { + mMainHandler.removeCallbacksAndMessages(null); + } } protected void setBottomPadding(int stackBottom, int lockIconPadding, int indicationPadding, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java index 30708a7cb2fe..ac66ad9e9c8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java @@ -97,6 +97,59 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test + public void highImportanceConversation() { + // GIVEN notification is high importance and is a people notification + final Notification notification = new Notification.Builder(mContext, "test") + .build(); + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .setImportance(IMPORTANCE_HIGH) + .build(); + when(mPeopleNotificationIdentifier + .getPeopleNotificationType(entry)) + .thenReturn(TYPE_PERSON); + + // THEN it is high priority conversation + assertTrue(mHighPriorityProvider.isHighPriorityConversation(entry)); + } + + @Test + public void lowImportanceConversation() { + // GIVEN notification is high importance and is a people notification + final Notification notification = new Notification.Builder(mContext, "test") + .build(); + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .setImportance(IMPORTANCE_LOW) + .build(); + when(mPeopleNotificationIdentifier + .getPeopleNotificationType(entry)) + .thenReturn(TYPE_PERSON); + + // THEN it is low priority conversation + assertFalse(mHighPriorityProvider.isHighPriorityConversation(entry)); + } + + @Test + public void highImportanceConversationWhenAnyOfChildIsHighPriority() { + // GIVEN notification is high importance and is a people notification + final NotificationEntry summary = createNotifEntry(false); + final NotificationEntry lowPriorityChild = createNotifEntry(false); + final NotificationEntry highPriorityChild = createNotifEntry(true); + when(mPeopleNotificationIdentifier + .getPeopleNotificationType(summary)) + .thenReturn(TYPE_PERSON); + final GroupEntry groupEntry = new GroupEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .setSummary(summary) + .setChildren(List.of(lowPriorityChild, highPriorityChild)) + .build(); + + // THEN the groupEntry is high priority conversation since it has a high priority child + assertTrue(mHighPriorityProvider.isHighPriorityConversation(groupEntry)); + } + + @Test public void messagingStyle() { // GIVEN notification is low importance but has messaging style final Notification notification = new Notification.Builder(mContext, "test") diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt index 742fcf5e03c3..55ea31571dfe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt @@ -17,6 +17,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.NotificationChannel +import android.app.NotificationManager.IMPORTANCE_DEFAULT +import android.app.NotificationManager.IMPORTANCE_HIGH +import android.app.NotificationManager.IMPORTANCE_LOW import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest @@ -31,10 +34,13 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.icon.ConversationIconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.withArgCaptor @@ -55,7 +61,8 @@ import org.mockito.Mockito.`when` as whenever class ConversationCoordinatorTest : SysuiTestCase() { // captured listeners and pluggables: private lateinit var promoter: NotifPromoter - private lateinit var peopleSectioner: NotifSectioner + private lateinit var peopleAlertingSectioner: NotifSectioner + private lateinit var peopleSilentSectioner: NotifSectioner private lateinit var peopleComparator: NotifComparator private lateinit var beforeRenderListListener: OnBeforeRenderListListener @@ -76,6 +83,7 @@ class ConversationCoordinatorTest : SysuiTestCase() { coordinator = ConversationCoordinator( peopleNotificationIdentifier, conversationIconManager, + HighPriorityProvider(peopleNotificationIdentifier, GroupMembershipManagerImpl()), headerController ) whenever(channel.isImportantConversation).thenReturn(true) @@ -90,12 +98,13 @@ class ConversationCoordinatorTest : SysuiTestCase() { verify(pipeline).addOnBeforeRenderListListener(capture()) } - peopleSectioner = coordinator.sectioner - peopleComparator = peopleSectioner.comparator!! + peopleAlertingSectioner = coordinator.peopleAlertingSectioner + peopleSilentSectioner = coordinator.peopleSilentSectioner + peopleComparator = peopleAlertingSectioner.comparator!! entry = NotificationEntryBuilder().setChannel(channel).build() - val section = NotifSection(peopleSectioner, 0) + val section = NotifSection(peopleAlertingSectioner, 0) entryA = NotificationEntryBuilder().setChannel(channel) .setSection(section).setTag("A").build() entryB = NotificationEntryBuilder().setChannel(channel) @@ -129,13 +138,67 @@ class ConversationCoordinatorTest : SysuiTestCase() { } @Test - fun testInPeopleSection() { + fun testInAlertingPeopleSectionWhenTheImportanceIsAtLeastDefault() { + // GIVEN + val alertingEntry = NotificationEntryBuilder().setChannel(channel) + .setImportance(IMPORTANCE_DEFAULT).build() + whenever(peopleNotificationIdentifier.getPeopleNotificationType(alertingEntry)) + .thenReturn(TYPE_PERSON) + + // put alerting people notifications in this section + assertThat(peopleAlertingSectioner.isInSection(alertingEntry)).isTrue() + } + + @Test + fun testInSilentPeopleSectionWhenTheImportanceIsLowerThanDefault() { + // GIVEN + val silentEntry = NotificationEntryBuilder().setChannel(channel) + .setImportance(IMPORTANCE_LOW).build() + whenever(peopleNotificationIdentifier.getPeopleNotificationType(silentEntry)) + .thenReturn(TYPE_PERSON) + + // THEN put silent people notifications in this section + assertThat(peopleSilentSectioner.isInSection(silentEntry)).isTrue() + // People Alerting sectioning happens before the silent one. + // It claims high important conversations and rest of conversations will be considered as silent. + assertThat(peopleAlertingSectioner.isInSection(silentEntry)).isFalse() + } + + @Test + fun testNotInPeopleSection() { + // GIVEN + val entry = NotificationEntryBuilder().setChannel(channel) + .setImportance(IMPORTANCE_LOW).build() + val importantEntry = NotificationEntryBuilder().setChannel(channel) + .setImportance(IMPORTANCE_HIGH).build() whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry)) - .thenReturn(TYPE_PERSON) + .thenReturn(TYPE_NON_PERSON) + whenever(peopleNotificationIdentifier.getPeopleNotificationType(importantEntry)) + .thenReturn(TYPE_NON_PERSON) - // only put people notifications in this section - assertTrue(peopleSectioner.isInSection(entry)) - assertFalse(peopleSectioner.isInSection(NotificationEntryBuilder().build())) + // THEN - only put people notification either silent or alerting + assertThat(peopleSilentSectioner.isInSection(entry)).isFalse() + assertThat(peopleAlertingSectioner.isInSection(importantEntry)).isFalse() + } + + @Test + fun testInAlertingPeopleSectionWhenThereIsAnImportantChild(){ + // GIVEN + val altChildA = NotificationEntryBuilder().setTag("A") + .setImportance(IMPORTANCE_DEFAULT).build() + val altChildB = NotificationEntryBuilder().setTag("B") + .setImportance(IMPORTANCE_LOW).build() + val summary = NotificationEntryBuilder().setId(2) + .setImportance(IMPORTANCE_LOW).setChannel(channel).build() + val groupEntry = GroupEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .setSummary(summary) + .setChildren(listOf(altChildA, altChildB)) + .build() + whenever(peopleNotificationIdentifier.getPeopleNotificationType(summary)) + .thenReturn(TYPE_PERSON) + // THEN + assertThat(peopleAlertingSectioner.isInSection(groupEntry)).isTrue() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index d5c0c5564af6..3d1253e2b05d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -52,7 +52,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; -import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; @@ -73,7 +72,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private HighPriorityProvider mHighPriorityProvider; - @Mock private SectionStyleProvider mSectionStyleProvider; @Mock private NotifPipeline mNotifPipeline; @Mock private NodeController mAlertingHeaderController; @Mock private NodeController mSilentNodeController; @@ -100,7 +98,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { mRankingCoordinator = new RankingCoordinator( mStatusBarStateController, mHighPriorityProvider, - mSectionStyleProvider, mAlertingHeaderController, mSilentHeaderController, mSilentNodeController); @@ -108,7 +105,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { mEntry.setRanking(getRankingForUnfilteredNotif().build()); mRankingCoordinator.attach(mNotifPipeline); - verify(mSectionStyleProvider).setMinimizedSections(any()); verify(mNotifPipeline, times(2)).addPreGroupFilter(mNotifFilterCaptor.capture()); mCapturedSuspendedFilter = mNotifFilterCaptor.getAllValues().get(0); mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt new file mode 100644 index 000000000000..cbb08946a1b0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt @@ -0,0 +1,78 @@ +package com.android.systemui.statusbar.notification.interruption + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_NOT_IMPORTANT_ENOUGH +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationInterruptStateProviderWrapperTest : SysuiTestCase() { + + @Test + fun decisionOfTrue() { + assertTrue(DecisionImpl.of(true).shouldInterrupt) + } + + @Test + fun decisionOfFalse() { + assertFalse(DecisionImpl.of(false).shouldInterrupt) + } + + @Test + fun decisionOfTrueInterned() { + assertEquals(DecisionImpl.of(true), DecisionImpl.of(true)) + } + + @Test + fun decisionOfFalseInterned() { + assertEquals(DecisionImpl.of(false), DecisionImpl.of(false)) + } + + @Test + fun fullScreenIntentDecisionShouldInterrupt() { + makeFsiDecision(FSI_DEVICE_NOT_INTERACTIVE).let { + assertTrue(it.shouldInterrupt) + assertFalse(it.wouldInterruptWithoutDnd) + } + } + + @Test + fun fullScreenIntentDecisionShouldNotInterrupt() { + makeFsiDecision(NO_FSI_NOT_IMPORTANT_ENOUGH).let { + assertFalse(it.shouldInterrupt) + assertFalse(it.wouldInterruptWithoutDnd) + } + } + + @Test + fun fullScreenIntentDecisionWouldInterruptWithoutDnd() { + makeFsiDecision(NO_FSI_SUPPRESSED_ONLY_BY_DND).let { + assertFalse(it.shouldInterrupt) + assertTrue(it.wouldInterruptWithoutDnd) + } + } + + @Test + fun fullScreenIntentDecisionWouldNotInterruptEvenWithoutDnd() { + makeFsiDecision(NO_FSI_SUPPRESSED_BY_DND).let { + assertFalse(it.shouldInterrupt) + assertFalse(it.wouldInterruptWithoutDnd) + } + } + + private fun makeFsiDecision(originalDecision: FullScreenIntentDecision) = + FullScreenIntentDecisionImpl(NotificationEntryBuilder().build(), originalDecision) +} 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 32f0adfa1954..48710a42f616 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 @@ -133,6 +133,7 @@ import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeControllerImpl; import com.android.systemui.shade.ShadeExpansionStateManager; +import com.android.systemui.shade.ShadeLogger; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; @@ -221,6 +222,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private NotificationListContainer mNotificationListContainer; @Mock private HeadsUpManagerPhone mHeadsUpManager; @Mock private NotificationPanelViewController mNotificationPanelViewController; + @Mock private ShadeLogger mShadeLogger; @Mock private NotificationPanelView mNotificationPanelView; @Mock private QuickSettingsController mQuickSettingsController; @Mock private IStatusBarService mBarService; @@ -469,6 +471,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mKeyguardViewMediator, new DisplayMetrics(), mMetricsLogger, + mShadeLogger, mUiBgExecutor, mNotificationMediaManager, mLockscreenUserManager, 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 1b6ab4d7af96..297cb9d691ba 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 @@ -179,15 +179,71 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test - fun iconId_cutout_whenDefaultDataDisabled() = + fun icon_usesLevelFromInteractor() = + testScope.runTest { + var latest: SignalIconModel? = null + val job = underTest.icon.onEach { latest = it }.launchIn(this) + + interactor.level.value = 3 + assertThat(latest!!.level).isEqualTo(3) + + interactor.level.value = 1 + assertThat(latest!!.level).isEqualTo(1) + + job.cancel() + } + + @Test + fun icon_usesNumberOfLevelsFromInteractor() = + testScope.runTest { + var latest: SignalIconModel? = null + val job = underTest.icon.onEach { latest = it }.launchIn(this) + + interactor.numberOfLevels.value = 5 + assertThat(latest!!.numberOfLevels).isEqualTo(5) + + interactor.numberOfLevels.value = 2 + assertThat(latest!!.numberOfLevels).isEqualTo(2) + + job.cancel() + } + + @Test + fun icon_defaultDataDisabled_showExclamationTrue() = testScope.runTest { interactor.setIsDefaultDataEnabled(false) var latest: SignalIconModel? = null val job = underTest.icon.onEach { latest = it }.launchIn(this) - val expected = defaultSignal(level = 1, connected = false) - assertThat(latest).isEqualTo(expected) + assertThat(latest!!.showExclamationMark).isTrue() + + job.cancel() + } + + @Test + fun icon_defaultConnectionFailed_showExclamationTrue() = + testScope.runTest { + interactor.isDefaultConnectionFailed.value = true + + var latest: SignalIconModel? = null + val job = underTest.icon.onEach { latest = it }.launchIn(this) + + assertThat(latest!!.showExclamationMark).isTrue() + + job.cancel() + } + + @Test + fun icon_enabledAndNotFailed_showExclamationFalse() = + testScope.runTest { + interactor.setIsDefaultDataEnabled(true) + interactor.isDefaultConnectionFailed.value = false + + var latest: SignalIconModel? = null + val job = underTest.icon.onEach { latest = it }.launchIn(this) + + assertThat(latest!!.showExclamationMark).isFalse() job.cancel() } @@ -572,16 +628,14 @@ class MobileIconViewModelTest : SysuiTestCase() { companion object { private const val SUB_1_ID = 1 + private const val NUM_LEVELS = 4 /** Convenience constructor for these tests */ - fun defaultSignal( - level: Int = 1, - connected: Boolean = true, - ): SignalIconModel { - return SignalIconModel(level, numberOfLevels = 4, showExclamationMark = !connected) + fun defaultSignal(level: Int = 1): SignalIconModel { + return SignalIconModel(level, NUM_LEVELS, showExclamationMark = false) } fun emptySignal(): SignalIconModel = - SignalIconModel(level = 0, numberOfLevels = 4, showExclamationMark = true) + SignalIconModel(level = 0, numberOfLevels = NUM_LEVELS, showExclamationMark = true) } } 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 ddc6d484d93f..d30e0246c2dd 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 @@ -29,6 +29,7 @@ import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -49,16 +50,14 @@ import com.android.systemui.util.mockito.nullable import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.After +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.ArgumentMatchers.anyInt @@ -80,9 +79,10 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var wifiManager: WifiManager private lateinit var executor: Executor - private lateinit var scope: CoroutineScope private lateinit var connectivityRepository: ConnectivityRepository + private val testScope = TestScope(UnconfinedTestDispatcher()) + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -96,7 +96,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { ) .thenReturn(flowOf(Unit)) executor = FakeExecutor(FakeSystemClock()) - scope = CoroutineScope(IMMEDIATE) connectivityRepository = ConnectivityRepositoryImpl( @@ -105,21 +104,16 @@ class WifiRepositoryImplTest : SysuiTestCase() { context, mock(), mock(), - scope, + testScope.backgroundScope, mock(), ) underTest = createRepo() } - @After - fun tearDown() { - scope.cancel() - } - @Test fun isWifiEnabled_initiallyGetsWifiManagerValue() = - runBlocking(IMMEDIATE) { + testScope.runTest { whenever(wifiManager.isWifiEnabled).thenReturn(true) underTest = createRepo() @@ -129,7 +123,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { // We need to call launch on the flows so that they start updating val networkJob = underTest.wifiNetwork.launchIn(this) val enabledJob = underTest.isWifiEnabled.launchIn(this) @@ -152,7 +146,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_networkLost_valueUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { // We need to call launch on the flows so that they start updating val networkJob = underTest.wifiNetwork.launchIn(this) val enabledJob = underTest.isWifiEnabled.launchIn(this) @@ -173,7 +167,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_intentsReceived_valueUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { val intentFlow = MutableSharedFlow<Unit>() whenever( broadcastDispatcher.broadcastFlow( @@ -203,7 +197,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() = - runBlocking(IMMEDIATE) { + testScope.runTest { val intentFlow = MutableSharedFlow<Unit>() whenever( broadcastDispatcher.broadcastFlow( @@ -242,7 +236,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_initiallyGetsDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) assertThat(underTest.isWifiDefault.value).isFalse() @@ -252,7 +246,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_wifiNetwork_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val wifiInfo = mock<WifiInfo>().apply { whenever(this.ssid).thenReturn(SSID) } @@ -268,7 +262,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/266628069. */ @Test fun isWifiDefault_transportInfoIsNotWifi_andNoWifiTransport_false() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val transportInfo = @@ -294,7 +288,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/266628069. */ @Test fun isWifiDefault_transportInfoIsNotWifi_butHasWifiTransport_true() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val transportInfo = @@ -319,7 +313,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaCellular_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val carrierMergedInfo = @@ -341,7 +335,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaCellular_withVcnTransport_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -360,7 +354,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaWifi_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val carrierMergedInfo = @@ -382,7 +376,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_carrierMergedViaWifi_withVcnTransport_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -401,7 +395,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_cellularAndWifiTransports_usesCellular_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -420,7 +414,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_cellularNotVcnNetwork_isFalse() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val capabilities = @@ -438,7 +432,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_isCarrierMergedViaUnderlyingWifi_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val underlyingNetwork = mock<Network>() @@ -473,7 +467,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_isCarrierMergedViaUnderlyingCellular_isTrue() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) val underlyingCarrierMergedNetwork = mock<Network>() @@ -507,7 +501,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiDefault_wifiNetworkLost_isFalse() = - runBlocking(IMMEDIATE) { + testScope.runTest { val job = underTest.isWifiDefault.launchIn(this) // First, add a network @@ -526,7 +520,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_initiallyGetsDefault() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -537,7 +531,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -561,7 +555,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -581,7 +575,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMergedViaUnderlyingWifi_flowHasCarrierMerged() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -618,7 +612,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMergedViaUnderlyingCellular_flowHasCarrierMerged() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -656,7 +650,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -680,7 +674,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_isCarrierMerged_getsCorrectValues() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -715,7 +709,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_notValidated_networkNotValidated() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -732,7 +726,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_validated_networkValidated() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -749,7 +743,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -770,7 +764,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/266628069. */ @Test fun wifiNetwork_transportInfoIsNotWifi_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -789,7 +783,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -811,7 +805,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -835,7 +829,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -854,7 +848,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_cellularAndWifiTransports_usesCellular() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -877,7 +871,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -910,7 +904,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -943,7 +937,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -986,7 +980,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1001,7 +995,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1020,7 +1014,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1043,7 +1037,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: WifiNetworkModel? = null val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) @@ -1069,7 +1063,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { /** Regression test for b/244173280. */ @Test fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest1: WifiNetworkModel? = null val job1 = underTest.wifiNetwork.onEach { latest1 = it }.launchIn(this) @@ -1096,8 +1090,151 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun isWifiConnectedWithValidSsid_inactiveNetwork_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.ssid).thenReturn(SSID) + // A non-primary network is inactive + whenever(this.isPrimary).thenReturn(false) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_carrierMergedNetwork_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_invalidNetwork_false() = + testScope.runTest { + val job = underTest.wifiNetwork.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(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_nullSsid_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn(null) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_unknownSsid_false() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn(UNKNOWN_SSID) + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_validSsid_true() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn("FakeSsid") + } + + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() + + job.cancel() + } + + @Test + fun isWifiConnectedWithValidSsid_activeToInactive_trueToFalse() = + testScope.runTest { + val job = underTest.wifiNetwork.launchIn(this) + + // Start with active + val wifiInfo = + mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.ssid).thenReturn("FakeSsid") + } + getNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() + + // WHEN the network is lost + getNetworkCallback().onLost(NETWORK) + + // THEN the isWifiConnected updates + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + + job.cancel() + } + + @Test fun wifiActivity_callbackGivesNone_activityFlowHasNone() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1111,7 +1248,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesIn_activityFlowHasIn() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1125,7 +1262,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesOut_activityFlowHasOut() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1139,7 +1276,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: DataActivityModel? = null val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) @@ -1159,7 +1296,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { logger, tableLogger, executor, - scope, + testScope.backgroundScope, wifiManager, ) } @@ -1204,5 +1341,3 @@ class WifiRepositoryImplTest : SysuiTestCase() { } } } - -private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt index ab4e93ceee84..4e0c309512e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.wifi.shared.model +import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -50,6 +51,42 @@ class WifiNetworkModelTest : SysuiTestCase() { WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1) } + @Test + fun active_hasValidSsid_nullSsid_false() { + val network = + WifiNetworkModel.Active( + NETWORK_ID, + level = MAX_VALID_LEVEL, + ssid = null, + ) + + assertThat(network.hasValidSsid()).isFalse() + } + + @Test + fun active_hasValidSsid_unknownSsid_false() { + val network = + WifiNetworkModel.Active( + NETWORK_ID, + level = MAX_VALID_LEVEL, + ssid = UNKNOWN_SSID, + ) + + assertThat(network.hasValidSsid()).isFalse() + } + + @Test + fun active_hasValidSsid_validSsid_true() { + val network = + WifiNetworkModel.Active( + NETWORK_ID, + level = MAX_VALID_LEVEL, + ssid = "FakeSsid", + ) + + assertThat(network.hasValidSsid()).isTrue() + } + // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt new file mode 100644 index 000000000000..9383a0a68844 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.qs + +import android.content.Context +import com.android.systemui.plugins.qs.QSFactory +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTileView + +class FakeQSFactory(private val tileCreator: (String) -> QSTile?) : QSFactory { + override fun createTile(tileSpec: String): QSTile? { + return tileCreator(tileSpec) + } + + override fun createTileView( + context: Context?, + tile: QSTile?, + collapsedView: Boolean + ): QSTileView { + throw NotImplementedError("Not implemented") + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt new file mode 100644 index 000000000000..777130409aad --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt @@ -0,0 +1,36 @@ +/* + * 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.qs.pipeline.data.repository + +import android.content.ComponentName + +class FakeCustomTileAddedRepository : CustomTileAddedRepository { + + private val tileAddedRegistry = mutableSetOf<Pair<Int, ComponentName>>() + + override fun isTileAdded(componentName: ComponentName, userId: Int): Boolean { + return (userId to componentName) in tileAddedRegistry + } + + override fun setTileAdded(componentName: ComponentName, userId: Int, added: Boolean) { + if (added) { + tileAddedRegistry.add(userId to componentName) + } else { + tileAddedRegistry.remove(userId to componentName) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt new file mode 100644 index 000000000000..2865710c2eae --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt @@ -0,0 +1,61 @@ +/* + * 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.qs.pipeline.data.repository + +import android.util.Log +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END +import com.android.systemui.qs.pipeline.shared.TileSpec +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTileSpecRepository : TileSpecRepository { + + private val tilesPerUser = mutableMapOf<Int, MutableStateFlow<List<TileSpec>>>() + + override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { + return getFlow(userId).asStateFlow().also { Log.d("Fabian", "Retrieving flow for $userId") } + } + + override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { + if (tile == TileSpec.Invalid) return + with(getFlow(userId)) { + value = + value.toMutableList().apply { + if (position == POSITION_AT_END) { + add(tile) + } else { + add(position, tile) + } + } + } + } + + override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) { + with(getFlow(userId)) { + value = + value.toMutableList().apply { removeAll(tiles.filter { it != TileSpec.Invalid }) } + } + } + + override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) { + getFlow(userId).value = tiles.filter { it != TileSpec.Invalid } + } + + private fun getFlow(userId: Int): MutableStateFlow<List<TileSpec>> = + tilesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ef7d5ae43396..9752ade1ce34 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13713,7 +13713,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (!sdkSandboxManagerLocal.canRegisterBroadcastReceiver( /*IntentFilter=*/ filter, flags, onlyProtectedBroadcasts)) { throw new SecurityException("SDK sandbox not allowed to register receiver" - + " with the given IntentFilter: " + filter.toString()); + + " with the given IntentFilter: " + filter.toLongString()); } } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index b25206d3b621..7e48f68dcefc 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -3391,6 +3391,7 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ public void onDefaultNetworkChanged(@NonNull Network network) { + mEventChanges.log("[UnderlyingNW] Default network changed to " + network); Log.d(TAG, "onDefaultNetworkChanged: " + network); // If there is a new default network brought up, cancel the retry task to prevent @@ -3628,6 +3629,7 @@ public class Vpn { mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities) .setTransportInfo(info) .build(); + mEventChanges.log("[VPNRunner] Update agent caps " + mNetworkCapabilities); doSendNetworkCapabilities(mNetworkAgent, mNetworkCapabilities); } } @@ -3664,6 +3666,7 @@ public class Vpn { private void startIkeSession(@NonNull Network underlyingNetwork) { Log.d(TAG, "Start new IKE session on network " + underlyingNetwork); + mEventChanges.log("[IKE] Start IKE session over " + underlyingNetwork); try { // Clear mInterface to prevent Ikev2VpnRunner being cleared when @@ -3778,6 +3781,7 @@ public class Vpn { } public void onValidationStatus(int status) { + mEventChanges.log("[Validation] validation status " + status); if (status == NetworkAgent.VALIDATION_STATUS_VALID) { // No data stall now. Reset it. mExecutor.execute(() -> { @@ -3818,6 +3822,7 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ public void onDefaultNetworkLost(@NonNull Network network) { + mEventChanges.log("[UnderlyingNW] Network lost " + network); // If the default network is torn down, there is no need to call // startOrMigrateIkeSession() since it will always check if there is an active network // can be used or not. @@ -3936,6 +3941,8 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ public void onSessionLost(int token, @Nullable Exception exception) { + mEventChanges.log("[IKE] Session lost on network " + mActiveNetwork + + (null == exception ? "" : " reason " + exception.getMessage())); Log.d(TAG, "onSessionLost() called for token " + token); if (!isActiveToken(token)) { @@ -4092,6 +4099,7 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ private void disconnectVpnRunner() { + mEventChanges.log("[VPNRunner] Disconnect runner, underlying network" + mActiveNetwork); mActiveNetwork = null; mUnderlyingNetworkCapabilities = null; mUnderlyingLinkProperties = null; diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java index c05a03ee1d2d..c76ca2beda96 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java @@ -286,7 +286,6 @@ final class AdditionalSubtypeUtils { final InputMethodSubtype.InputMethodSubtypeBuilder builder = new InputMethodSubtype.InputMethodSubtypeBuilder() .setSubtypeNameResId(label) - .setSubtypeNameOverride(untranslatableName) .setPhysicalKeyboardHint( pkLanguageTag == null ? null : new ULocale(pkLanguageTag), pkLayoutType == null ? "" : pkLayoutType) @@ -302,6 +301,9 @@ final class AdditionalSubtypeUtils { if (subtypeId != InputMethodSubtype.SUBTYPE_ID_NONE) { builder.setSubtypeId(subtypeId); } + if (untranslatableName != null) { + builder.setSubtypeNameOverride(untranslatableName); + } tempSubtypesArray.add(builder.build()); } } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index f0e8ede5987d..94d5aabe24e5 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -357,12 +357,16 @@ public final class MediaProjectionManagerService extends SystemService } catch (NameNotFoundException e) { throw new IllegalArgumentException("No package matching :" + packageName); } - - projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion, - ai.isPrivilegedApp()); - if (isPermanentGrant) { - mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, - projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); + final long callingToken = Binder.clearCallingIdentity(); + try { + projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion, + ai.isPrivilegedApp()); + if (isPermanentGrant) { + mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, + projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); + } + } finally { + Binder.restoreCallingIdentity(callingToken); } return projection; } @@ -418,16 +422,9 @@ public final class MediaProjectionManagerService extends SystemService if (packageName == null || packageName.isEmpty()) { throw new IllegalArgumentException("package name must not be empty"); } - MediaProjection projection; final UserHandle callingUser = Binder.getCallingUserHandle(); - final long callingToken = Binder.clearCallingIdentity(); - try { - projection = createProjectionInternal(uid, packageName, type, isPermanentGrant, - callingUser, false); - } finally { - Binder.restoreCallingIdentity(callingToken); - } - return projection; + return createProjectionInternal(uid, packageName, type, isPermanentGrant, + callingUser, false); } @Override // Binder call diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6d27fe058423..5d81dda5f5eb 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3815,6 +3815,28 @@ public class NotificationManagerService extends SystemService { } @Override + public boolean canUseFullScreenIntent(@NonNull AttributionSource attributionSource) { + final String packageName = attributionSource.getPackageName(); + final int uid = attributionSource.getUid(); + final int userId = UserHandle.getUserId(uid); + checkCallerIsSameApp(packageName, uid, userId); + + final ApplicationInfo applicationInfo; + try { + applicationInfo = mPackageManagerClient.getApplicationInfoAsUser( + packageName, PackageManager.MATCH_DIRECT_BOOT_AUTO, userId); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Failed to getApplicationInfo() in canUseFullScreenIntent()", e); + return false; + } + final boolean showStickyHunIfDenied = mFlagResolver.isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags + .SHOW_STICKY_HUN_FOR_DENIED_FSI); + return checkUseFullScreenIntentPermission(attributionSource, applicationInfo, + showStickyHunIfDenied /* isAppOpPermission */, false /* forDataDelivery */); + } + + @Override public void updateNotificationChannelGroupForPackage(String pkg, int uid, NotificationChannelGroup group) throws RemoteException { enforceSystemOrSystemUI("Caller not system or systemui"); @@ -6826,36 +6848,28 @@ public class NotificationManagerService extends SystemService { notification.flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED; - if (notification.fullScreenIntent != null && ai.targetSdkVersion >= Build.VERSION_CODES.Q) { + if (notification.fullScreenIntent != null) { final boolean forceDemoteFsiToStickyHun = mFlagResolver.isEnabled( SystemUiSystemPropertiesFlags.NotificationFlags.FSI_FORCE_DEMOTE); - - final boolean showStickyHunIfDenied = mFlagResolver.isEnabled( - SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI); - if (forceDemoteFsiToStickyHun) { makeStickyHun(notification, pkg, userId); - - } else if (showStickyHunIfDenied) { - final AttributionSource source = new AttributionSource.Builder(notificationUid) - .setPackageName(pkg) - .build(); - - final int permissionResult = mPermissionManager.checkPermissionForDataDelivery( - Manifest.permission.USE_FULL_SCREEN_INTENT, source, /* message= */ null); - - if (permissionResult != PermissionManager.PERMISSION_GRANTED) { - makeStickyHun(notification, pkg, userId); - } - } else { - int fullscreenIntentPermission = getContext().checkPermission( - android.Manifest.permission.USE_FULL_SCREEN_INTENT, -1, notificationUid); - - if (fullscreenIntentPermission != PERMISSION_GRANTED) { - notification.fullScreenIntent = null; - Slog.w(TAG, "Package " + pkg + ": Use of fullScreenIntent requires the" - + "USE_FULL_SCREEN_INTENT permission"); + final AttributionSource attributionSource = + new AttributionSource.Builder(notificationUid).setPackageName(pkg).build(); + final boolean showStickyHunIfDenied = mFlagResolver.isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags + .SHOW_STICKY_HUN_FOR_DENIED_FSI); + final boolean canUseFullScreenIntent = checkUseFullScreenIntentPermission( + attributionSource, ai, showStickyHunIfDenied /* isAppOpPermission */, + true /* forDataDelivery */); + if (!canUseFullScreenIntent) { + if (showStickyHunIfDenied) { + makeStickyHun(notification, pkg, userId); + } else { + notification.fullScreenIntent = null; + Slog.w(TAG, "Package " + pkg + ": Use of fullScreenIntent requires the" + + "USE_FULL_SCREEN_INTENT permission"); + } } } } @@ -6951,6 +6965,30 @@ public class NotificationManagerService extends SystemService { ai.packageName) == AppOpsManager.MODE_ALLOWED; } + private boolean checkUseFullScreenIntentPermission(@NonNull AttributionSource attributionSource, + @NonNull ApplicationInfo applicationInfo, boolean isAppOpPermission, + boolean forDataDelivery) { + if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q) { + return true; + } + if (isAppOpPermission) { + final int permissionResult; + if (forDataDelivery) { + permissionResult = mPermissionManager.checkPermissionForDataDelivery( + permission.USE_FULL_SCREEN_INTENT, attributionSource, /* message= */ null); + } else { + permissionResult = mPermissionManager.checkPermissionForPreflight( + permission.USE_FULL_SCREEN_INTENT, attributionSource); + } + return permissionResult == PermissionManager.PERMISSION_GRANTED; + } else { + final int permissionResult = getContext().checkPermission( + permission.USE_FULL_SCREEN_INTENT, attributionSource.getPid(), + attributionSource.getUid()); + return permissionResult == PERMISSION_GRANTED; + } + } + private void checkRemoteViews(String pkg, String tag, int id, Notification notification) { if (removeRemoteView(pkg, tag, id, notification.contentView)) { notification.contentView = null; diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index c29e4d78f929..52fdbda04fcd 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -606,6 +606,7 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { final Computer snapshot = snapshot(); // Return null for InstantApps. if (snapshot.getInstantAppPackageName(Binder.getCallingUid()) != null) { + Log.w(PackageManagerService.TAG, "Returning null PackageInstaller for InstantApps"); return null; } return mInstallerService; diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 5f424edb15c4..596e9b964643 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3588,6 +3588,11 @@ final class InstallPackageHelper { // remove the package from the system and re-scan it without any // special privileges mRemovePackageHelper.removePackage(pkg, true); + PackageSetting ps = mPm.mSettings.getPackageLPr(packageName); + if (ps != null) { + ps.getPkgState().setUpdatedSystemApp(false); + } + try { final File codePath = new File(pkg.getPath()); synchronized (mPm.mInstallLock) { diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index ad77ef7ca975..9127a93a46ee 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -187,6 +187,9 @@ public final class SuspendPackageHelper { if (changed) { changedPackagesList.add(packageName); changedUids.add(UserHandle.getUid(userId, packageState.getAppId())); + } else { + Slog.w(TAG, "No change is needed for package: " + packageName + + ". Skipping suspending/un-suspending."); } } diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java index c9ebeaee88ce..5015985dd747 100644 --- a/services/core/java/com/android/server/pm/VerifyingSession.java +++ b/services/core/java/com/android/server/pm/VerifyingSession.java @@ -361,7 +361,8 @@ final class VerifyingSession { } final int verifierUserId = verifierUser.getIdentifier(); - List<String> requiredVerifierPackages = Arrays.asList(mPm.mRequiredVerifierPackages); + List<String> requiredVerifierPackages = new ArrayList<>( + Arrays.asList(mPm.mRequiredVerifierPackages)); boolean requiredVerifierPackagesOverridden = false; // Allow verifier override for ADB installations which could already be unverified using diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index b1b0c559aad4..4a03628ab8e5 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3569,6 +3569,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private void dumpWallpaper(WallpaperData wallpaper, PrintWriter pw) { if (wallpaper == null) { pw.println(" (null entry)"); + return; } pw.print(" User "); pw.print(wallpaper.userId); pw.print(": id="); pw.print(wallpaper.wallpaperId); diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index fa3a186a6153..df360b86fdf8 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -44,6 +44,8 @@ import static com.android.server.accessibility.AccessibilityTraceProto.WHERE; import static com.android.server.accessibility.AccessibilityTraceProto.WINDOW_MANAGER_SERVICE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import static com.android.server.wm.WindowManagerService.dumpSparseArray; +import static com.android.server.wm.WindowManagerService.dumpSparseArrayValues; import static com.android.server.wm.WindowTracing.WINSCOPE_EXT; import android.accessibilityservice.AccessibilityTrace; @@ -542,15 +544,12 @@ final class AccessibilityController { } void dump(PrintWriter pw, String prefix) { - for (int i = 0; i < mDisplayMagnifiers.size(); i++) { - final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.valueAt(i); - if (displayMagnifier != null) { - displayMagnifier.dump(pw, prefix - + "Magnification display# " + mDisplayMagnifiers.keyAt(i)); - } - } - pw.println(prefix - + "mWindowsForAccessibilityObserver=" + mWindowsForAccessibilityObserver); + dumpSparseArray(pw, prefix, mDisplayMagnifiers, "magnification display", + (index, key) -> pw.printf("%sDisplay #%d:", prefix + " ", key), + dm -> dm.dump(pw, "")); + dumpSparseArrayValues(pw, prefix, mWindowsForAccessibilityObserver, + "windows for accessibility observer"); + mAccessibilityWindowsPopulator.dump(pw, prefix); } void onFocusChanged(InputTarget lastTarget, InputTarget newTarget) { diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java index 21b241a0d117..afe164056ff4 100644 --- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java +++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static com.android.server.wm.WindowManagerService.ValueDumper; +import static com.android.server.wm.WindowManagerService.dumpSparseArray; import static com.android.server.wm.utils.RegionUtils.forEachRect; import android.annotation.NonNull; @@ -39,7 +41,9 @@ import android.view.WindowManager; import android.window.WindowInfosListener; import com.android.internal.annotations.GuardedBy; +import com.android.server.wm.WindowManagerService.KeyDumper; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -562,6 +566,35 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { notifyWindowsChanged(displayIdsForWindowsChanged); } + void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.println("AccessibilityWindowsPopulator"); + String prefix2 = prefix + " "; + + pw.print(prefix2); pw.print("mWindowsNotificationEnabled: "); + pw.println(mWindowsNotificationEnabled); + + if (mVisibleWindows.isEmpty()) { + pw.print(prefix2); pw.println("No visible windows"); + } else { + pw.print(prefix2); pw.print(mVisibleWindows.size()); + pw.print(" visible windows: "); pw.println(mVisibleWindows); + } + KeyDumper noKeyDumper = (i, k) -> {}; // display id is already shown on value; + KeyDumper displayDumper = (i, d) -> pw.printf("%sDisplay #%d: ", prefix, d); + // Ideally magnificationSpecDumper should use spec.dump(pw), but there is no such method + ValueDumper<MagnificationSpec> magnificationSpecDumper = spec -> pw.print(spec); + + dumpSparseArray(pw, prefix2, mDisplayInfos, "display info", noKeyDumper, d -> pw.print(d)); + dumpSparseArray(pw, prefix2, mInputWindowHandlesOnDisplays, "window handles on display", + displayDumper, list -> pw.print(list)); + dumpSparseArray(pw, prefix2, mMagnificationSpecInverseMatrix, "magnification spec matrix", + noKeyDumper, matrix -> matrix.dump(pw)); + dumpSparseArray(pw, prefix2, mCurrentMagnificationSpec, "current magnification spec", + noKeyDumper, magnificationSpecDumper); + dumpSparseArray(pw, prefix2, mPreviousMagnificationSpec, "previous magnification spec", + noKeyDumper, magnificationSpecDumper); + } + @GuardedBy("mLock") private void releaseResources() { mInputWindowHandlesOnDisplays.clear(); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 8346e7c83190..fb1f8994dbc5 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3522,7 +3522,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final boolean endTask = task.getTopNonFinishingActivity() == null && !task.isClearingToReuseTask(); - mTransitionController.requestCloseTransitionIfNeeded(endTask ? task : this); + final Transition newTransition = + mTransitionController.requestCloseTransitionIfNeeded(endTask ? task : this); if (isState(RESUMED)) { if (endTask) { mAtmService.getTaskChangeNotificationController().notifyTaskRemovalStarted( @@ -3576,7 +3577,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else if (!isState(PAUSING)) { if (mVisibleRequested) { // Prepare and execute close transition. - prepareActivityHideTransitionAnimation(); + if (mTransitionController.isShellTransitionsEnabled()) { + setVisibility(false); + if (newTransition != null) { + // This is a transition specifically for this close operation, so set + // ready now. + newTransition.setReady(mDisplayContent, true); + } + } else { + prepareActivityHideTransitionAnimation(); + } } final boolean removedActivity = completeFinishing("finishIfPossible") == null; @@ -7917,6 +7927,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged( task.mTaskId, requestedOrientation); + + mDisplayContent.getDisplayRotation().onSetRequestedOrientation(); } /* @@ -9760,6 +9772,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * directly with keeping its record. */ void restartProcessIfVisible() { + if (finishing) return; Slog.i(TAG, "Request to restart process of " + this); // Reset the existing override configuration so it can be updated according to the latest diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index d4f151f5c66d..38f13ec15987 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1606,6 +1606,8 @@ class ActivityStarter { transitionController.requestStartTransition(newTransition, mTargetTask == null ? started.getTask() : mTargetTask, remoteTransition, null /* displayChange */); + } else if (result == START_SUCCESS && mStartActivity.isState(RESUMED)) { + // Do nothing if the activity is started and is resumed directly. } else if (isStarted) { // Make the collecting transition wait until this request is ready. transitionController.setReady(started, false); diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 48cf567ba9be..d916a1be1a03 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -23,6 +23,7 @@ import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Handler; import android.os.Trace; import android.util.ArraySet; import android.util.Slog; @@ -172,7 +173,7 @@ class BLASTSyncEngine { if (ran) { return; } - mWm.mH.removeCallbacks(this); + mHandler.removeCallbacks(this); ran = true; SurfaceControl.Transaction t = new SurfaceControl.Transaction(); for (WindowContainer wc : wcAwaitingCommit) { @@ -199,13 +200,13 @@ class BLASTSyncEngine { }; CommitCallback callback = new CommitCallback(); merged.addTransactionCommittedListener((r) -> { r.run(); }, callback::onCommitted); - mWm.mH.postDelayed(callback, BLAST_TIMEOUT_DURATION); + mHandler.postDelayed(callback, BLAST_TIMEOUT_DURATION); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady"); mListener.onTransactionReady(mSyncId, merged); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); mActiveSyncs.remove(mSyncId); - mWm.mH.removeCallbacks(mOnTimeout); + mHandler.removeCallbacks(mOnTimeout); // Immediately start the next pending sync-transaction if there is one. if (mActiveSyncs.size() == 0 && !mPendingSyncSets.isEmpty()) { @@ -216,7 +217,7 @@ class BLASTSyncEngine { throw new IllegalStateException("Pending Sync Set didn't start a sync."); } // Post this so that the now-playing transition setup isn't interrupted. - mWm.mH.post(() -> { + mHandler.post(() -> { synchronized (mWm.mGlobalLock) { pt.mApplySync.run(); } @@ -228,7 +229,7 @@ class BLASTSyncEngine { if (mReady == ready) { return; } - ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready", mSyncId); + ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Set ready %b", mSyncId, ready); mReady = ready; if (!ready) return; mWm.mWindowPlacerLocked.requestTraversal(); @@ -269,6 +270,7 @@ class BLASTSyncEngine { } private final WindowManagerService mWm; + private final Handler mHandler; private int mNextSyncId = 0; private final SparseArray<SyncGroup> mActiveSyncs = new SparseArray<>(); @@ -280,7 +282,13 @@ class BLASTSyncEngine { private final ArrayList<PendingSyncSet> mPendingSyncSets = new ArrayList<>(); BLASTSyncEngine(WindowManagerService wms) { + this(wms, wms.mH); + } + + @VisibleForTesting + BLASTSyncEngine(WindowManagerService wms, Handler mainHandler) { mWm = wms; + mHandler = mainHandler; } /** @@ -305,8 +313,8 @@ class BLASTSyncEngine { if (mActiveSyncs.size() != 0) { // We currently only support one sync at a time, so start a new SyncGroup when there is // another may cause issue. - ProtoLog.w(WM_DEBUG_SYNC_ENGINE, - "SyncGroup %d: Started when there is other active SyncGroup", s.mSyncId); + Slog.e(TAG, "SyncGroup " + s.mSyncId + + ": Started when there is other active SyncGroup"); } mActiveSyncs.put(s.mSyncId, s); ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Started for listener: %s", @@ -325,7 +333,7 @@ class BLASTSyncEngine { @VisibleForTesting void scheduleTimeout(SyncGroup s, long timeoutMs) { - mWm.mH.postDelayed(s.mOnTimeout, timeoutMs); + mHandler.postDelayed(s.mOnTimeout, timeoutMs); } void addToSyncSet(int id, WindowContainer wc) { diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index be80b010962b..7b562b0bc964 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -286,8 +286,8 @@ class BackNavigationController { currentActivity.getCustomAnimation(false/* open */); if (customAppTransition != null) { infoBuilder.setCustomAnimation(currentActivity.packageName, - customAppTransition.mExitAnim, customAppTransition.mEnterAnim, + customAppTransition.mExitAnim, customAppTransition.mBackgroundColor); } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index bec58b848478..c2bc4591ce0d 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -775,6 +775,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ DisplayWindowPolicyControllerHelper mDwpcHelper; + private final DisplayRotationReversionController mRotationReversionController; + private final Consumer<WindowState> mUpdateWindowsForAnimator = w -> { WindowStateAnimator winAnimator = w.mWinAnimator; final ActivityRecord activity = w.mActivityRecord; @@ -1204,6 +1206,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled( /* checkDeviceConfig */ false) ? new DisplayRotationCompatPolicy(this) : null; + mRotationReversionController = new DisplayRotationReversionController(this); mInputMonitor = new InputMonitor(mWmService, this); mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this); @@ -1333,6 +1336,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .show(mA11yOverlayLayer); } + DisplayRotationReversionController getRotationReversionController() { + return mRotationReversionController; + } + boolean isReady() { // The display is ready when the system and the individual display are both ready. return mWmService.mDisplayReady && mDisplayReady; @@ -1711,9 +1718,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } private boolean updateOrientation(boolean forceUpdate) { + final WindowContainer prevOrientationSource = mLastOrientationSource; final int orientation = getOrientation(); // The last orientation source is valid only after getOrientation. final WindowContainer orientationSource = getLastOrientationSource(); + if (orientationSource != prevOrientationSource + && mRotationReversionController.isRotationReversionEnabled()) { + mRotationReversionController.updateForNoSensorOverride(); + } final ActivityRecord r = orientationSource != null ? orientationSource.asActivityRecord() : null; if (r != null) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 628f4d3a85d6..20048ce543f3 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -33,6 +33,9 @@ import static com.android.server.wm.DisplayRotationProto.IS_FIXED_TO_USER_ROTATI import static com.android.server.wm.DisplayRotationProto.LAST_ORIENTATION; import static com.android.server.wm.DisplayRotationProto.ROTATION; import static com.android.server.wm.DisplayRotationProto.USER_ROTATION; +import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT; +import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_HALF_FOLD; +import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_NOSENSOR; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.WINDOWS_FREEZING_SCREENS_ACTIVE; @@ -97,6 +100,8 @@ public class DisplayRotation { // config changes and unexpected jumps while folding the device to closed state. private static final int FOLDING_RECOMPUTE_CONFIG_DELAY_MS = 800; + private static final int ROTATION_UNDEFINED = -1; + private static class RotationAnimationPair { @AnimRes int mEnter; @@ -104,6 +109,9 @@ public class DisplayRotation { int mExit; } + @Nullable + final FoldController mFoldController; + private final WindowManagerService mService; private final DisplayContent mDisplayContent; private final DisplayPolicy mDisplayPolicy; @@ -125,8 +133,6 @@ public class DisplayRotation { private OrientationListener mOrientationListener; private StatusBarManagerInternal mStatusBarManagerInternal; private SettingsObserver mSettingsObserver; - @Nullable - private FoldController mFoldController; @NonNull private final DeviceStateController mDeviceStateController; @NonNull @@ -189,6 +195,12 @@ public class DisplayRotation { */ private int mShowRotationSuggestions; + /** + * The most recent {@link Surface.Rotation} choice shown to the user for confirmation, or + * {@link #ROTATION_UNDEFINED} + */ + private int mRotationChoiceShownToUserForConfirmation = ROTATION_UNDEFINED; + private static final int ALLOW_ALL_ROTATIONS_UNDEFINED = -1; private static final int ALLOW_ALL_ROTATIONS_DISABLED = 0; private static final int ALLOW_ALL_ROTATIONS_ENABLED = 1; @@ -291,7 +303,11 @@ public class DisplayRotation { if (mSupportAutoRotation && mContext.getResources().getBoolean( R.bool.config_windowManagerHalfFoldAutoRotateOverride)) { mFoldController = new FoldController(); + } else { + mFoldController = null; } + } else { + mFoldController = null; } } @@ -349,6 +365,11 @@ public class DisplayRotation { return -1; } + @VisibleForTesting + boolean useDefaultSettingsProvider() { + return isDefaultDisplay; + } + /** * Updates the configuration which may have different values depending on current user, e.g. * runtime resource overlay. @@ -894,7 +915,8 @@ public class DisplayRotation { @VisibleForTesting void setUserRotation(int userRotationMode, int userRotation) { - if (isDefaultDisplay) { + mRotationChoiceShownToUserForConfirmation = ROTATION_UNDEFINED; + if (useDefaultSettingsProvider()) { // We'll be notified via settings listener, so we don't need to update internal values. final ContentResolver res = mContext.getContentResolver(); final int accelerometerRotation = @@ -1613,6 +1635,17 @@ public class DisplayRotation { } } + /** + * Called from {@link ActivityRecord#setRequestedOrientation(int)} + */ + void onSetRequestedOrientation() { + if (mCompatPolicyForImmersiveApps == null + || mRotationChoiceShownToUserForConfirmation == ROTATION_UNDEFINED) { + return; + } + mOrientationListener.onProposedRotationChanged(mRotationChoiceShownToUserForConfirmation); + } + void dump(String prefix, PrintWriter pw) { pw.println(prefix + "DisplayRotation"); pw.println(prefix + " mCurrentAppOrientation=" @@ -1839,7 +1872,7 @@ public class DisplayRotation { return false; } if (mDeviceState == DeviceStateController.DeviceState.HALF_FOLDED) { - return !(isTabletop ^ mTabletopRotations.contains(mRotation)); + return isTabletop == mTabletopRotations.contains(mRotation); } return true; } @@ -1863,14 +1896,17 @@ public class DisplayRotation { return mDeviceState == DeviceStateController.DeviceState.OPEN && !mShouldIgnoreSensorRotation // Ignore if the hinge angle still moving && mInHalfFoldTransition - && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted. + && mDisplayContent.getRotationReversionController().isOverrideActive( + REVERSION_TYPE_HALF_FOLD) && mUserRotationMode - == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked. + == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked. } int revertOverriddenRotation() { int savedRotation = mHalfFoldSavedRotation; mHalfFoldSavedRotation = -1; + mDisplayContent.getRotationReversionController() + .revertOverride(REVERSION_TYPE_HALF_FOLD); mInHalfFoldTransition = false; return savedRotation; } @@ -1890,6 +1926,8 @@ public class DisplayRotation { && mDeviceState != DeviceStateController.DeviceState.HALF_FOLDED) { // The device has transitioned to HALF_FOLDED state: save the current rotation and // update the device rotation. + mDisplayContent.getRotationReversionController().beforeOverrideApplied( + REVERSION_TYPE_HALF_FOLD); mHalfFoldSavedRotation = mRotation; mDeviceState = newState; // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will @@ -2012,9 +2050,11 @@ public class DisplayRotation { mService.mPowerManagerInternal.setPowerBoost(Boost.INTERACTION, 0); dispatchProposedRotation(rotation); if (isRotationChoiceAllowed(rotation)) { + mRotationChoiceShownToUserForConfirmation = rotation; final boolean isValid = isValidRotationChoice(rotation); sendProposedRotationChangeToStatusBarInternal(rotation, isValid); } else { + mRotationChoiceShownToUserForConfirmation = ROTATION_UNDEFINED; mService.updateRotation(false /* alwaysSendConfiguration */, false /* forceRelayout */); } @@ -2093,6 +2133,8 @@ public class DisplayRotation { final int mHalfFoldSavedRotation; final boolean mInHalfFoldTransition; final DeviceStateController.DeviceState mDeviceState; + @Nullable final boolean[] mRotationReversionSlots; + @Nullable final String mDisplayRotationCompatPolicySummary; Record(DisplayRotation dr, int fromRotation, int toRotation) { @@ -2133,6 +2175,8 @@ public class DisplayRotation { ? null : dc.mDisplayRotationCompatPolicy .getSummaryForDisplayRotationHistoryRecord(); + mRotationReversionSlots = + dr.mDisplayContent.getRotationReversionController().getSlotsCopy(); } void dump(String prefix, PrintWriter pw) { @@ -2158,6 +2202,12 @@ public class DisplayRotation { if (mDisplayRotationCompatPolicySummary != null) { pw.println(prefix + mDisplayRotationCompatPolicySummary); } + if (mRotationReversionSlots != null) { + pw.println(prefix + " reversionSlots= NOSENSOR " + + mRotationReversionSlots[REVERSION_TYPE_NOSENSOR] + ", CAMERA " + + mRotationReversionSlots[REVERSION_TYPE_CAMERA_COMPAT] + " HALF_FOLD " + + mRotationReversionSlots[REVERSION_TYPE_HALF_FOLD]); + } } } diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index fb72d6c6b56d..ae93a9496f7c 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -33,6 +33,7 @@ import static android.view.Display.TYPE_INTERNAL; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; +import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -156,6 +157,11 @@ final class DisplayRotationCompatPolicy { @ScreenOrientation int getOrientation() { mLastReportedOrientation = getOrientationInternal(); + if (mLastReportedOrientation != SCREEN_ORIENTATION_UNSPECIFIED) { + rememberOverriddenOrientationIfNeeded(); + } else { + restoreOverriddenOrientationIfNeeded(); + } return mLastReportedOrientation; } @@ -277,6 +283,34 @@ final class DisplayRotationCompatPolicy { + " }"; } + private void restoreOverriddenOrientationIfNeeded() { + if (!isOrientationOverridden()) { + return; + } + if (mDisplayContent.getRotationReversionController().revertOverride( + REVERSION_TYPE_CAMERA_COMPAT)) { + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Reverting orientation after camera compat force rotation"); + // Reset last orientation source since we have reverted the orientation. + mDisplayContent.mLastOrientationSource = null; + } + } + + private boolean isOrientationOverridden() { + return mDisplayContent.getRotationReversionController().isOverrideActive( + REVERSION_TYPE_CAMERA_COMPAT); + } + + private void rememberOverriddenOrientationIfNeeded() { + if (!isOrientationOverridden()) { + mDisplayContent.getRotationReversionController().beforeOverrideApplied( + REVERSION_TYPE_CAMERA_COMPAT); + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Saving original orientation before camera compat, last orientation is %d", + mDisplayContent.getLastOrientation()); + } + } + // Refreshing only when configuration changes after rotation. private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig, Configuration lastReportedConfig) { diff --git a/services/core/java/com/android/server/wm/DisplayRotationReversionController.java b/services/core/java/com/android/server/wm/DisplayRotationReversionController.java new file mode 100644 index 000000000000..d3a8a82f8f87 --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayRotationReversionController.java @@ -0,0 +1,148 @@ +/* + * 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; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; + +import android.annotation.Nullable; +import android.app.WindowConfiguration; +import android.content.ActivityInfoProto; +import android.view.Surface; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.server.policy.WindowManagerPolicy; + +/** + * Defines the behavior of reversion from device rotation overrides. + * + * <p>There are 3 override types: + * <ol> + * <li>The top application has {@link SCREEN_ORIENTATION_NOSENSOR} set and is rotated to + * {@link ROTATION_0}. + * <li>Camera compat treatment has rotated the app {@link DisplayRotationCompatPolicy}. + * <li>The device is half-folded and has auto-rotate is temporarily enabled. + * </ol> + * + * <p>Before an override is enabled, a component should call {@code beforeOverrideApplied}. When + * it wishes to revert, it should call {@code revertOverride}. The user rotation will be restored + * if there are no other overrides present. + */ +final class DisplayRotationReversionController { + + static final int REVERSION_TYPE_NOSENSOR = 0; + static final int REVERSION_TYPE_CAMERA_COMPAT = 1; + static final int REVERSION_TYPE_HALF_FOLD = 2; + private static final int NUM_SLOTS = 3; + + @Surface.Rotation + private int mUserRotationOverridden = WindowConfiguration.ROTATION_UNDEFINED; + @WindowManagerPolicy.UserRotationMode + private int mUserRotationModeOverridden; + + private final boolean[] mSlots = new boolean[NUM_SLOTS]; + private final DisplayContent mDisplayContent; + + DisplayRotationReversionController(DisplayContent content) { + mDisplayContent = content; + } + + boolean isRotationReversionEnabled() { + return mDisplayContent.mDisplayRotationCompatPolicy != null + || mDisplayContent.getDisplayRotation().mFoldController != null + || mDisplayContent.getIgnoreOrientationRequest(); + } + + void beforeOverrideApplied(int slotIndex) { + if (mSlots[slotIndex]) return; + maybeSaveUserRotation(); + mSlots[slotIndex] = true; + } + + boolean isOverrideActive(int slotIndex) { + return mSlots[slotIndex]; + } + + @Nullable + boolean[] getSlotsCopy() { + return isRotationReversionEnabled() ? mSlots.clone() : null; + } + + void updateForNoSensorOverride() { + if (!mSlots[REVERSION_TYPE_NOSENSOR]) { + if (isTopFullscreenActivityNoSensor()) { + ProtoLog.v(WM_DEBUG_ORIENTATION, "NOSENSOR override detected"); + beforeOverrideApplied(REVERSION_TYPE_NOSENSOR); + } + } else { + if (!isTopFullscreenActivityNoSensor()) { + ProtoLog.v(WM_DEBUG_ORIENTATION, "NOSENSOR override is absent: reverting"); + revertOverride(REVERSION_TYPE_NOSENSOR); + } + } + } + + boolean isAnyOverrideActive() { + for (int i = 0; i < NUM_SLOTS; ++i) { + if (mSlots[i]) { + return true; + } + } + return false; + } + + boolean revertOverride(int slotIndex) { + if (!mSlots[slotIndex]) return false; + mSlots[slotIndex] = false; + if (isAnyOverrideActive()) { + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Other orientation overrides are in place: not reverting"); + return false; + } + // Only override if the rotation is frozen and there are no other active slots. + if (mDisplayContent.getDisplayRotation().isRotationFrozen()) { + mDisplayContent.getDisplayRotation().setUserRotation( + mUserRotationModeOverridden, + mUserRotationOverridden); + return true; + } else { + return false; + } + } + + private void maybeSaveUserRotation() { + if (!isAnyOverrideActive()) { + mUserRotationModeOverridden = + mDisplayContent.getDisplayRotation().getUserRotationMode(); + mUserRotationOverridden = mDisplayContent.getDisplayRotation().getUserRotation(); + } + } + + private boolean isTopFullscreenActivityNoSensor() { + final Task topFullscreenTask = + mDisplayContent.getTask( + t -> t.getWindowingMode() == WINDOWING_MODE_FULLSCREEN); + if (topFullscreenTask != null) { + final ActivityRecord topActivity = + topFullscreenTask.topRunningActivity(); + return topActivity != null && topActivity.getOrientation() + == ActivityInfoProto.SCREEN_ORIENTATION_NOSENSOR; + } + return false; + } +} diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index f8f0211e108f..f5079d37b324 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -1512,7 +1512,7 @@ class RecentTasks { // callbacks here. final Task removedTask = mTasks.remove(removeIndex); if (removedTask != task) { - if (removedTask.hasChild()) { + if (removedTask.hasChild() && !removedTask.isActivityTypeHome()) { Slog.i(TAG, "Add " + removedTask + " to hidden list because adding " + task); // A non-empty task is replaced by a new task. Because the removed task is no longer // managed by the recent tasks list, add it to the hidden list to prevent the task diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 0857898ca1d2..73f4b5beea50 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -4687,7 +4687,7 @@ class Task extends TaskFragment { if (!isAttached()) { return; } - mTransitionController.collect(this); + mTransitionController.recordTaskOrder(this); final TaskDisplayArea taskDisplayArea = getDisplayArea(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 3cc154892e33..e209ef97fd7b 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -521,10 +521,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mChanges.put(wc, info); } mParticipants.add(wc); - if (wc.getDisplayContent() != null && !mTargetDisplays.contains(wc.getDisplayContent())) { - mTargetDisplays.add(wc.getDisplayContent()); - addOnTopTasks(wc.getDisplayContent(), mOnTopTasksStart); - } + recordDisplay(wc.getDisplayContent()); if (info.mShowWallpaper) { // Collect the wallpaper token (for isWallpaper(wc)) so it is part of the sync set. final WindowState wallpaper = @@ -535,6 +532,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + private void recordDisplay(DisplayContent dc) { + if (dc == null || mTargetDisplays.contains(dc)) return; + mTargetDisplays.add(dc); + addOnTopTasks(dc, mOnTopTasksStart); + } + + /** + * Records information about the initial task order. This does NOT collect anything. Call this + * before any ordering changes *could* occur, but it is not known yet if it will occur. + */ + void recordTaskOrder(WindowContainer from) { + recordDisplay(from.getDisplayContent()); + } + /** Adds the top non-alwaysOnTop tasks within `task` to `out`. */ private static void addOnTopTasks(Task task, ArrayList<Task> out) { for (int i = task.getChildCount() - 1; i >= 0; --i) { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index bcb8c46de5ed..8d5660701994 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -577,12 +577,16 @@ class TransitionController { return transition; } - /** Requests transition for a window container which will be removed or invisible. */ - void requestCloseTransitionIfNeeded(@NonNull WindowContainer<?> wc) { - if (mTransitionPlayer == null) return; + /** + * Requests transition for a window container which will be removed or invisible. + * @return the new transition if it was created for this request, `null` otherwise. + */ + Transition requestCloseTransitionIfNeeded(@NonNull WindowContainer<?> wc) { + if (mTransitionPlayer == null) return null; + Transition out = null; if (wc.isVisibleRequested()) { if (!isCollecting()) { - requestStartTransition(createTransition(TRANSIT_CLOSE, 0 /* flags */), + out = requestStartTransition(createTransition(TRANSIT_CLOSE, 0 /* flags */), wc.asTask(), null /* remoteTransition */, null /* displayChange */); } collectExistenceChange(wc); @@ -591,6 +595,7 @@ class TransitionController { // collecting, this should be a member just in case. collect(wc); } + return out; } /** @see Transition#collect */ @@ -605,6 +610,12 @@ class TransitionController { mCollectingTransition.collectExistenceChange(wc); } + /** @see Transition#recordTaskOrder */ + void recordTaskOrder(@NonNull WindowContainer wc) { + if (mCollectingTransition == null) return; + mCollectingTransition.recordTaskOrder(wc); + } + /** * Collects the window containers which need to be synced with the changing display area into * the current collecting transition. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index cd4d6e4f1600..82c057b99c10 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -235,6 +235,7 @@ import android.util.EventLog; import android.util.MergedConfiguration; import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; @@ -6697,9 +6698,8 @@ public class WindowManagerService extends IWindowManager.Stub mInputManagerCallback.dump(pw, " "); mSnapshotController.dump(pw, " "); - if (mAccessibilityController.hasCallbacks()) { - mAccessibilityController.dump(pw, " "); - } + + dumpAccessibilityController(pw, /* force= */ false); if (dumpAll) { final WindowState imeWindow = mRoot.getCurrentInputMethodWindow(); @@ -6736,6 +6736,23 @@ public class WindowManagerService extends IWindowManager.Stub } } + private void dumpAccessibilityController(PrintWriter pw, boolean force) { + boolean hasCallbacks = mAccessibilityController.hasCallbacks(); + if (!hasCallbacks && !force) { + return; + } + if (!hasCallbacks) { + pw.println("AccessibilityController doesn't have callbacks, but printing it anways:"); + } else { + pw.println("AccessibilityController:"); + } + mAccessibilityController.dump(pw, " "); + } + + private void dumpAccessibilityLocked(PrintWriter pw) { + dumpAccessibilityController(pw, /* force= */ true); + } + private boolean dumpWindows(PrintWriter pw, String name, boolean dumpAll) { final ArrayList<WindowState> windows = new ArrayList(); if ("apps".equals(name) || "visible".equals(name) || "visible-apps".equals(name)) { @@ -6855,6 +6872,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.println(" d[isplays]: active display contents"); pw.println(" t[okens]: token list"); pw.println(" w[indows]: window list"); + pw.println(" a11y[accessibility]: accessibility-related state"); pw.println(" package-config: installed packages having app-specific config"); pw.println(" trace: print trace status and write Winscope trace to file"); pw.println(" cmd may also be a NAME to dump windows. NAME may"); @@ -6918,6 +6936,11 @@ public class WindowManagerService extends IWindowManager.Stub dumpWindowsLocked(pw, true, null); } return; + } else if ("accessibility".equals(cmd) || "a11y".equals(cmd)) { + synchronized (mGlobalLock) { + dumpAccessibilityLocked(pw); + } + return; } else if ("all".equals(cmd)) { synchronized (mGlobalLock) { dumpWindowsLocked(pw, true, null); @@ -9437,4 +9460,53 @@ public class WindowManagerService extends IWindowManager.Stub return List.copyOf(notifiedApps); } } + + // TODO(b/271188189): move dump stuff below to common code / add unit tests + + interface ValueDumper<T> { + void dump(T value); + } + + interface KeyDumper{ + void dump(int index, int key); + } + + static void dumpSparseArray(PrintWriter pw, String prefix, SparseArray<?> array, String name) { + dumpSparseArray(pw, prefix, array, name, /* keyDumper= */ null, /* valuedumper= */ null); + } + + static <T> void dumpSparseArrayValues(PrintWriter pw, String prefix, SparseArray<T> array, + String name) { + dumpSparseArray(pw, prefix, array, name, (i, k) -> {}, /* valueDumper= */ null); + } + + static <T> void dumpSparseArray(PrintWriter pw, String prefix, SparseArray<T> array, + String name, @Nullable KeyDumper keyDumper, @Nullable ValueDumper<T> valueDumper) { + int size = array.size(); + if (size == 0) { + pw.print(prefix); pw.print("No "); pw.print(name); pw.println("s"); + return; + } + pw.print(prefix); pw.print(size); pw.print(' '); + pw.print(name); pw.print(size > 1 ? "s" : ""); pw.println(':'); + + String prefix2 = prefix + " "; + for (int i = 0; i < size; i++) { + int key = array.keyAt(i); + T value = array.valueAt(i); + if (keyDumper != null) { + keyDumper.dump(i, key); + } else { + pw.print(prefix2); pw.print(i); pw.print(": "); pw.print(key); pw.print("->"); + } + if (value == null) { + pw.print("(null)"); + } else if (valueDumper != null) { + valueDumper.dump(value); + } else { + pw.print(value); + } + pw.println(); + } + } } diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index 5c77aa22ece8..19a0c5e8adcb 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -27,7 +27,7 @@ import android.credentials.ui.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; -import android.util.Log; +import android.util.Slog; import java.util.ArrayList; import java.util.Set; @@ -67,7 +67,8 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta .createNewSession(mContext, mUserId, providerInfo, this, remoteCredentialService); if (providerClearSession != null) { - Log.i(TAG, "In startProviderSession - provider session created and being added"); + Slog.d(TAG, "In startProviderSession - provider session created " + + "and being added for: " + providerInfo.getComponentName()); mProviders.put(providerClearSession.getComponentName().flattenToString(), providerClearSession); } @@ -77,12 +78,12 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta @Override // from provider session public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Log.i(TAG, "in onStatusChanged with status: " + status); + Slog.d(TAG, "in onStatusChanged with status: " + status + ", and source: " + source); if (ProviderSession.isTerminatingStatus(status)) { - Log.i(TAG, "in onStatusChanged terminating status"); + Slog.d(TAG, "in onProviderStatusChanged terminating status"); onProviderTerminated(componentName); } else if (ProviderSession.isCompletionStatus(status)) { - Log.i(TAG, "in onStatusChanged isCompletionStatus status"); + Slog.d(TAG, "in onProviderStatusChanged isCompletionStatus status"); onProviderResponseComplete(componentName); } } diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 02aaf867fa7b..a04143afadcd 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -33,7 +33,7 @@ import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; -import android.util.Log; +import android.util.Slog; import com.android.server.credentials.metrics.ProviderStatusForMetrics; @@ -77,7 +77,8 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR .createNewSession(mContext, mUserId, providerInfo, this, remoteCredentialService); if (providerCreateSession != null) { - Log.i(TAG, "In startProviderSession - provider session created and being added"); + Slog.d(TAG, "In initiateProviderSession - provider session created and " + + "being added for: " + providerInfo.getComponentName()); mProviders.put(providerCreateSession.getComponentName().flattenToString(), providerCreateSession); } @@ -120,7 +121,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable CreateCredentialResponse response) { - Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); + Slog.d(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.collectChosenMetricViaCandidateTransfer(mProviders.get( componentName.flattenToString()).mProviderSessionMetric @@ -163,13 +164,13 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Log.i(TAG, "in onProviderStatusChanged with status: " + status); + Slog.d(TAG, "in onStatusChanged with status: " + status + ", and source: " + source); // If all provider responses have been received, we can either need the UI, // or we need to respond with error. The only other case is the entry being // selected after the UI has been invoked which has a separate code path. if (!isAnyProviderPending()) { if (isUiInvocationNeeded()) { - Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); + Slog.d(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); getProviderDataAndInitiateUi(); } else { respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREATE_OPTIONS, diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index c44e665ba699..aeb4801628f2 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -30,7 +30,7 @@ import android.credentials.ui.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; -import android.util.Log; +import android.util.Slog; import com.android.server.credentials.metrics.ProviderStatusForMetrics; @@ -77,7 +77,8 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, .createNewSession(mContext, mUserId, providerInfo, this, remoteCredentialService); if (providerGetSession != null) { - Log.i(TAG, "In startProviderSession - provider session created and being added"); + Slog.d(TAG, "In startProviderSession - provider session created and " + + "being added for: " + providerInfo.getComponentName()); mProviders.put(providerGetSession.getComponentName().flattenToString(), providerGetSession); } @@ -116,7 +117,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { - Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); + Slog.d(TAG, "onFinalResponseReceived from: " + componentName.flattenToString()); mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.collectChosenMetricViaCandidateTransfer( mProviders.get(componentName.flattenToString()) @@ -160,7 +161,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { - Log.i(TAG, "in onStatusChanged with status: " + status + "and source: " + source); + Slog.d(TAG, "in onStatusChanged with status: " + status + ", and source: " + source); // Auth entry was selected, and it did not have any underlying credentials if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) { @@ -173,7 +174,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, // or we need to respond with error. The only other case is the entry being // selected after the UI has been invoked which has a separate code path. if (isUiInvocationNeeded()) { - Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); + Slog.d(TAG, "in onProviderStatusChanged - isUiInvocationNeeded"); getProviderDataAndInitiateUi(); } else { respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index f274e65a20c3..9e7a87e74522 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -33,7 +33,6 @@ import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; -import android.util.Log; import android.util.Slog; import java.util.ArrayList; @@ -67,6 +66,9 @@ public class PrepareGetRequestSession extends GetRequestSession { @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName, ProviderSession.CredentialsSource source) { + Slog.d(TAG, "in onProviderStatusChanged with status: " + status + ", and " + + "source: " + source); + switch (source) { case REMOTE_PROVIDER: // Remote provider's status changed. We should check if all providers are done, and @@ -123,7 +125,7 @@ public class PrepareGetRequestSession extends GetRequestSession { hasPermission, credentialTypes, hasAuthenticationResults, hasRemoteResults, uiIntent)); } catch (RemoteException e) { - Log.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e); + Slog.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e); } } @@ -138,7 +140,7 @@ public class PrepareGetRequestSession extends GetRequestSession { /*hasRemoteResults=*/ false, /*pendingIntent=*/ null)); } catch (RemoteException e) { - Log.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e); + Slog.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e); } } @@ -179,10 +181,8 @@ public class PrepareGetRequestSession extends GetRequestSession { private PendingIntent getUiIntent() { ArrayList<ProviderData> providerDataList = new ArrayList<>(); for (ProviderSession session : mProviders.values()) { - Log.i(TAG, "preparing data for : " + session.getComponentName()); ProviderData providerData = session.prepareUiData(); if (providerData != null) { - Log.i(TAG, "Provider data is not null"); providerDataList.add(providerData); } } diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index e98c5241ae00..8fd02691e190 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -32,7 +32,6 @@ import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.service.credentials.CallingAppInfo; -import android.util.Log; import android.util.Slog; import com.android.internal.R; @@ -179,7 +178,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential @Override // from CredentialManagerUiCallbacks public void onUiSelection(UserSelectionDialogResult selection) { if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); + Slog.w(TAG, "Request has already been completed. This is strange."); return; } if (isSessionCancelled()) { @@ -187,13 +186,11 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential return; } String providerId = selection.getProviderId(); - Log.i(TAG, "onUiSelection, providerId: " + providerId); ProviderSession providerSession = mProviders.get(providerId); if (providerSession == null) { - Log.i(TAG, "providerSession not found in onUiSelection"); + Slog.w(TAG, "providerSession not found in onUiSelection. This is strange."); return; } - Log.i(TAG, "Provider session found"); mRequestSessionMetric.collectMetricPerBrowsingSelect(selection, providerSession.mProviderSessionMetric.getCandidatePhasePerProviderMetric()); providerSession.onUiEntrySelected(selection.getEntryKey(), @@ -247,15 +244,13 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential void getProviderDataAndInitiateUi() { ArrayList<ProviderData> providerDataList = getProviderDataForUi(); if (!providerDataList.isEmpty()) { - Log.i(TAG, "provider list not empty about to initiate ui"); launchUiWithProviderData(providerDataList); } } @NonNull protected ArrayList<ProviderData> getProviderDataForUi() { - Log.i(TAG, "In getProviderDataAndInitiateUi"); - Log.i(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size()); + Slog.d(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size()); ArrayList<ProviderData> providerDataList = new ArrayList<>(); mRequestSessionMetric.logCandidatePhaseMetrics(mProviders); @@ -265,10 +260,8 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential } for (ProviderSession session : mProviders.values()) { - Log.i(TAG, "preparing data for : " + session.getComponentName()); ProviderData providerData = session.prepareUiData(); if (providerData != null) { - Log.i(TAG, "Provider data is not null"); providerDataList.add(providerData); } } @@ -284,7 +277,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*has_exception=*/ false, ProviderStatusForMetrics.FINAL_SUCCESS); if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); + Slog.w(TAG, "Request has already been completed. This is strange."); return; } if (isSessionCancelled()) { @@ -300,7 +293,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential } catch (RemoteException e) { mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); - Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); + Slog.e(TAG, "Issue while responding to client with a response : " + e); mRequestSessionMetric.logApiCalledAtFinish( /*apiStatus=*/ ApiStatus.FAILURE.getMetricCode()); } @@ -317,7 +310,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); + Slog.w(TAG, "Request has already been completed. This is strange."); return; } if (isSessionCancelled()) { @@ -330,7 +323,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential try { invokeClientCallbackError(errorType, errorMsg); } catch (RemoteException e) { - Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); + Slog.e(TAG, "Issue while responding to client with error : " + e); } boolean isUserCanceled = errorType.contains(MetricUtilities.USER_CANCELED_SUBSTRING); mRequestSessionMetric.logFailureOrUserCancel(isUserCanceled); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index b8a21ec4c030..341b331b74e0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -589,12 +589,18 @@ public class ActivityRecordTests extends WindowTestsBase { throw new IllegalStateException("Orientation in new config should be either" + "landscape or portrait."); } + + final DisplayRotation displayRotation = activity.mDisplayContent.getDisplayRotation(); + spyOn(displayRotation); + activity.setRequestedOrientation(requestedOrientation); final ActivityConfigurationChangeItem expected = ActivityConfigurationChangeItem.obtain(newConfig); verify(mAtm.getLifecycleManager()).scheduleTransaction(eq(activity.app.getThread()), eq(activity.token), eq(expected)); + + verify(displayRotation).onSetRequestedOrientation(); } @Test 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 ba9f809e9a2a..7330411d1dd7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -24,6 +24,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.ActivityInfo.FLAG_SHOW_WHEN_LOCKED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; 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_UNSPECIFIED; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR; @@ -1063,6 +1064,51 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(SCREEN_ORIENTATION_LANDSCAPE, dc.getOrientation()); } + private void updateAllDisplayContentAndRotation(DisplayContent dc) { + // NB updateOrientation will not revert the user orientation until a settings change + // takes effect. + dc.updateOrientation(); + dc.onDisplayChanged(dc); + dc.mWmService.updateRotation(true /* alwaysSendConfiguration */, + false /* forceRelayout */); + waitUntilHandlersIdle(); + } + + @Test + public void testNoSensorRevert() { + final DisplayContent dc = mDisplayContent; + spyOn(dc); + doReturn(true).when(dc).getIgnoreOrientationRequest(); + final DisplayRotation dr = dc.getDisplayRotation(); + spyOn(dr); + doReturn(false).when(dr).useDefaultSettingsProvider(); + final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build(); + app.setOrientation(SCREEN_ORIENTATION_LANDSCAPE, app); + + assertFalse(dc.getRotationReversionController().isAnyOverrideActive()); + dc.getDisplayRotation().setUserRotation(WindowManagerPolicy.USER_ROTATION_LOCKED, + ROTATION_90); + updateAllDisplayContentAndRotation(dc); + assertEquals(ROTATION_90, dc.getDisplayRotation() + .rotationForOrientation(SCREEN_ORIENTATION_UNSPECIFIED, ROTATION_90)); + + app.setOrientation(SCREEN_ORIENTATION_NOSENSOR); + updateAllDisplayContentAndRotation(dc); + assertTrue(dc.getRotationReversionController().isAnyOverrideActive()); + assertEquals(ROTATION_0, dc.getRotation()); + + app.setOrientation(SCREEN_ORIENTATION_UNSPECIFIED); + updateAllDisplayContentAndRotation(dc); + assertFalse(dc.getRotationReversionController().isAnyOverrideActive()); + assertEquals(WindowManagerPolicy.USER_ROTATION_LOCKED, + dc.getDisplayRotation().getUserRotationMode()); + assertEquals(ROTATION_90, dc.getDisplayRotation().getUserRotation()); + assertEquals(ROTATION_90, dc.getDisplayRotation() + .rotationForOrientation(SCREEN_ORIENTATION_UNSPECIFIED, ROTATION_0)); + dc.getDisplayRotation().setUserRotation(WindowManagerPolicy.USER_ROTATION_FREE, + ROTATION_0); + } + @Test public void testOnDescendantOrientationRequestChanged() { final DisplayContent dc = createNewDisplay(); 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 c2b3783b7311..a3117269eb01 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -365,6 +365,23 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test + public void testCameraDisconnected_revertRotationAndRefresh() throws Exception { + configureActivityAndDisplay(SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_LANDSCAPE); + // Open camera and test for compat treatment + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_LANDSCAPE); + assertActivityRefreshRequested(/* refreshRequested */ true); + // Close camera and test for revert + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test public void testGetOrientation_cameraConnectionClosed_returnUnspecified() { configureActivity(SCREEN_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 495f868b1b11..4b2d1071d113 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -70,6 +70,7 @@ import android.view.IRotationWatcher; import android.view.Surface; import android.view.WindowManager; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; @@ -114,6 +115,7 @@ public class DisplayRotationTests { private static WindowManagerService sMockWm; private DisplayContent mMockDisplayContent; + private DisplayRotationReversionController mMockDisplayRotationReversionController; private DisplayPolicy mMockDisplayPolicy; private DisplayAddress mMockDisplayAddress; private Context mMockContext; @@ -140,6 +142,8 @@ public class DisplayRotationTests { private DeviceStateController mDeviceStateController; private TestDisplayRotation mTarget; + @Nullable + private DisplayRotationImmersiveAppCompatPolicy mDisplayRotationImmersiveAppCompatPolicyMock; @BeforeClass public static void setUpOnce() { @@ -165,7 +169,7 @@ public class DisplayRotationTests { LocalServices.removeServiceForTest(StatusBarManagerInternal.class); mMockStatusBarManagerInternal = mock(StatusBarManagerInternal.class); LocalServices.addService(StatusBarManagerInternal.class, mMockStatusBarManagerInternal); - + mDisplayRotationImmersiveAppCompatPolicyMock = null; mBuilder = new DisplayRotationBuilder(); } @@ -578,6 +582,38 @@ public class DisplayRotationTests { } @Test + public void testNotifiesChoiceWhenSensorUpdates_immersiveApp() throws Exception { + mDisplayRotationImmersiveAppCompatPolicyMock = mock( + DisplayRotationImmersiveAppCompatPolicy.class); + when(mDisplayRotationImmersiveAppCompatPolicyMock.isRotationLockEnforced( + Surface.ROTATION_90)).thenReturn(true); + + mBuilder.build(); + configureDisplayRotation(SCREEN_ORIENTATION_PORTRAIT, false, false); + + thawRotation(); + + enableOrientationSensor(); + + mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_90)); + assertTrue(waitForUiHandler()); + + verify(mMockStatusBarManagerInternal).onProposedRotationChanged(Surface.ROTATION_90, true); + + // An imaginary ActivityRecord.setRequestedOrientation call disables immersive mode: + when(mDisplayRotationImmersiveAppCompatPolicyMock.isRotationLockEnforced( + Surface.ROTATION_90)).thenReturn(false); + + // And then ActivityRecord.setRequestedOrientation calls onSetRequestedOrientation. + mTarget.onSetRequestedOrientation(); + + // onSetRequestedOrientation should lead to a second call to + // mOrientationListener.onProposedRotationChanged + // but now, instead of notifying mMockStatusBarManagerInternal, it calls updateRotation: + verify(sMockWm).updateRotation(false, false); + } + + @Test public void testAllowAllRotations_allowsUpsideDownSuggestion() throws Exception { mBuilder.build(); @@ -1374,6 +1410,10 @@ public class DisplayRotationTests { when(mMockContext.getResources().getBoolean( com.android.internal.R.bool.config_windowManagerHalfFoldAutoRotateOverride)) .thenReturn(mSupportHalfFoldAutoRotateOverride); + mMockDisplayRotationReversionController = + mock(DisplayRotationReversionController.class); + when(mMockDisplayContent.getRotationReversionController()) + .thenReturn(mMockDisplayRotationReversionController); mMockResolver = mock(ContentResolver.class); when(mMockContext.getContentResolver()).thenReturn(mMockResolver); @@ -1404,7 +1444,7 @@ public class DisplayRotationTests { } } - private static class TestDisplayRotation extends DisplayRotation { + private class TestDisplayRotation extends DisplayRotation { IntConsumer mProposedRotationCallback; TestDisplayRotation(DisplayContent dc, DisplayAddress address, DisplayPolicy policy, @@ -1417,7 +1457,7 @@ public class DisplayRotationTests { @Override DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( WindowManagerService service, DisplayContent displayContent) { - return null; + return mDisplayRotationImmersiveAppCompatPolicyMock; } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index 8e91ca28fcf1..77efc4b0d561 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -42,6 +42,8 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; +import com.android.server.testutils.TestHandler; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -371,6 +373,49 @@ public class SyncEngineTests extends WindowTestsBase { mAppWindow.removeImmediately(); } + @Test + public void testQueueSyncSet() { + final TestHandler testHandler = new TestHandler(null); + TestWindowContainer mockWC = new TestWindowContainer(mWm, true /* waiter */); + TestWindowContainer mockWC2 = new TestWindowContainer(mWm, true /* waiter */); + + final BLASTSyncEngine bse = createTestBLASTSyncEngine(testHandler); + + BLASTSyncEngine.TransactionReadyListener listener = mock( + BLASTSyncEngine.TransactionReadyListener.class); + + int id = startSyncSet(bse, listener); + bse.addToSyncSet(id, mockWC); + bse.setReady(id); + bse.onSurfacePlacement(); + verify(listener, times(0)).onTransactionReady(eq(id), notNull()); + + final int[] nextId = new int[]{-1}; + bse.queueSyncSet( + () -> nextId[0] = startSyncSet(bse, listener), + () -> { + bse.setReady(nextId[0]); + bse.addToSyncSet(nextId[0], mockWC2); + }); + + // Make sure it is queued + assertEquals(-1, nextId[0]); + + // Finish the original sync and see that we've started a new sync-set immediately but + // that the readiness was posted. + mockWC.onSyncFinishedDrawing(); + verify(mWm.mWindowPlacerLocked).requestTraversal(); + bse.onSurfacePlacement(); + verify(listener, times(1)).onTransactionReady(eq(id), notNull()); + + assertTrue(nextId[0] != -1); + assertFalse(bse.isReady(nextId[0])); + + // now make sure the applySync callback was posted. + testHandler.flush(); + assertTrue(bse.isReady(nextId[0])); + } + static int startSyncSet(BLASTSyncEngine engine, BLASTSyncEngine.TransactionReadyListener listener) { return engine.startSyncSet(listener, BLAST_TIMEOUT_DURATION, "Test"); 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 7e3ec55f262a..f85cdf0b5035 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -77,6 +77,7 @@ import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; @@ -886,7 +887,11 @@ class WindowTestsBase extends SystemServiceTestsBase { } BLASTSyncEngine createTestBLASTSyncEngine() { - return new BLASTSyncEngine(mWm) { + return createTestBLASTSyncEngine(mWm.mH); + } + + BLASTSyncEngine createTestBLASTSyncEngine(Handler handler) { + return new BLASTSyncEngine(mWm, handler) { @Override void scheduleTimeout(SyncGroup s, long timeoutMs) { // Disable timeout. diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java index f3ef834168b5..52ff90f38113 100644 --- a/telecomm/java/android/telecom/CallAttributes.java +++ b/telecomm/java/android/telecom/CallAttributes.java @@ -59,7 +59,10 @@ public final class CallAttributes implements Parcelable { public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities"; /** @hide **/ - public static final String CALLER_PID = "CallerPid"; + public static final String CALLER_PID_KEY = "CallerPid"; + + /** @hide **/ + public static final String CALLER_UID_KEY = "CallerUid"; private CallAttributes(@NonNull PhoneAccountHandle phoneAccountHandle, @NonNull CharSequence displayName, diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index cbdf38ae95d4..ee9d6c1a2448 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2989,4 +2989,14 @@ interface ITelephony { * {@code false} otherwise. */ boolean setSatelliteServicePackageName(in String servicePackageName); + + /** + * This API can be used by only CTS to update the timeout duration in milliseconds that + * satellite should stay at listening mode to wait for the next incoming page before disabling + * listening mode. + * + * @param timeoutMillis The timeout duration in millisecond. + * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise. + */ + boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis); } diff --git a/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml b/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml index d2653d0de0d4..f20dd424c617 100644 --- a/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml +++ b/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml @@ -18,6 +18,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android"> <gradient android:startColor="#000000" - android:endColor="#181818" + android:endColor="#222222" android:angle="0"/> </shape>
\ No newline at end of file |