diff options
266 files changed, 6151 insertions, 5864 deletions
diff --git a/cmds/am/am.sh b/cmds/am/am.sh index 76ec214cb446..f099be3e26a2 100755 --- a/cmds/am/am.sh +++ b/cmds/am/am.sh @@ -1,11 +1,10 @@ #!/system/bin/sh -# set to top-app process group -settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true - if [ "$1" != "instrument" ] ; then cmd activity "$@" else + # set to top-app process group for instrument + settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true base=/system export CLASSPATH=$base/framework/am.jar exec app_process $base/bin com.android.commands.am.Am "$@" diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java index 6310d32515c5..696bc82a9ffc 100644 --- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java +++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java @@ -18,6 +18,7 @@ package com.android.commands.bmgr; import android.annotation.IntDef; import android.annotation.UserIdInt; +import android.app.ActivityManager; import android.app.backup.BackupManager; import android.app.backup.BackupManagerMonitor; import android.app.backup.BackupProgress; @@ -73,6 +74,8 @@ public class Bmgr { "Error: Could not access the backup transport. Is the system running?"; private static final String PM_NOT_RUNNING_ERR = "Error: Could not access the Package Manager. Is the system running?"; + private static final String INVALID_USER_ID_ERR_TEMPLATE = + "Error: Invalid user id (%d).\n"; private String[] mArgs; private int mNextArg; @@ -104,6 +107,11 @@ public class Bmgr { mArgs = args; mNextArg = 0; int userId = parseUserId(); + if (userId < 0) { + System.err.printf(INVALID_USER_ID_ERR_TEMPLATE, userId); + return; + } + String op = nextArg(); Slog.v(TAG, "Running " + op + " for user:" + userId); @@ -955,12 +963,15 @@ public class Bmgr { private int parseUserId() { String arg = nextArg(); - if ("--user".equals(arg)) { - return UserHandle.parseUserArg(nextArg()); - } else { + if (!"--user".equals(arg)) { mNextArg--; return UserHandle.USER_SYSTEM; } + int userId = UserHandle.parseUserArg(nextArg()); + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + return userId; } private static void showUsage() { diff --git a/cmds/uinput/tests/Android.bp b/cmds/uinput/tests/Android.bp index e728bd270a46..516de3325f77 100644 --- a/cmds/uinput/tests/Android.bp +++ b/cmds/uinput/tests/Android.bp @@ -18,3 +18,17 @@ android_test { "device-tests", ], } + +android_ravenwood_test { + name: "UinputTestsRavenwood", + srcs: [ + "src/**/*.java", + ], + static_libs: [ + "androidx.test.runner", + "frameworks-base-testutils", + "platform-test-annotations", + "truth", + "uinput", + ], +} diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index dce15b833bbb..3633b4eb333a 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -11370,7 +11370,7 @@ public class Notification implements Parcelable if (mProgressPoints == null) { mProgressPoints = new ArrayList<>(); } - if (point.getPosition() >= 0) { + if (point.getPosition() > 0) { mProgressPoints.add(point); if (mProgressPoints.size() > MAX_PROGRESS_POINT_LIMIT) { @@ -11379,7 +11379,7 @@ public class Notification implements Parcelable } } else { - Log.w(TAG, "Dropped the point. The position is a negative integer."); + Log.w(TAG, "Dropped the point. The position is a negative or zero integer."); } return this; @@ -11893,7 +11893,9 @@ public class Notification implements Parcelable final List<Point> points = new ArrayList<>(); for (Point point : mProgressPoints) { final int position = point.getPosition(); - if (position < 0 || position > totalLength) continue; + // The points at start/end aren't supposed to show in the progress bar. + // Therefore those are also dropped here. + if (position <= 0 || position >= totalLength) continue; points.add(sanitizePoint(point, backgroundColor, defaultProgressColor)); if (points.size() == MAX_PROGRESS_POINT_LIMIT) { break; diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 566e78a8de35..2b0e941cf602 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -277,6 +277,23 @@ public final class CompanionDeviceManager { */ public static final int MESSAGE_ONEWAY_TO_WEARABLE = 0x43847987; // +TOW + + /** @hide */ + @IntDef(flag = true, prefix = { "TRANSPORT_FLAG_" }, value = { + TRANSPORT_FLAG_EXTEND_PATCH_DIFF, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransportFlags {} + + /** + * A security flag that allows transports to be attached to devices that may be more vulnerable + * due to infrequent updates. Can only be used for associations with + * {@link AssociationRequest#DEVICE_PROFILE_WEARABLE_SENSING} device profile. + * + * @hide + */ + public static final int TRANSPORT_FLAG_EXTEND_PATCH_DIFF = 1; + /** * Callback for applications to receive updates about and the outcome of * {@link AssociationRequest} issued via {@code associate()} call. @@ -1452,7 +1469,52 @@ public final class CompanionDeviceManager { } try { - final Transport transport = new Transport(associationId, in, out); + final Transport transport = new Transport(associationId, in, out, 0); + mTransports.put(associationId, transport); + transport.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to attach transport", e); + } + } + } + + /** + * Attach a bidirectional communication stream to be used as a transport channel for + * transporting system data between associated devices. Flags can be provided to further + * customize the behavior of the transport. + * + * @param associationId id of the associated device. + * @param in Already connected stream of data incoming from remote + * associated device. + * @param out Already connected stream of data outgoing to remote associated + * device. + * @param flags Flags to customize transport behavior. + * @throws DeviceNotAssociatedException Thrown if the associationId was not previously + * associated with this app. + * + * @see #buildPermissionTransferUserConsentIntent(int) + * @see #startSystemDataTransfer(int, Executor, OutcomeReceiver) + * @see #detachSystemDataTransport(int) + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) + public void attachSystemDataTransport(int associationId, + @NonNull InputStream in, + @NonNull OutputStream out, + @TransportFlags int flags) throws DeviceNotAssociatedException { + if (mService == null) { + Log.w(TAG, "CompanionDeviceManager service is not available."); + return; + } + + synchronized (mTransports) { + if (mTransports.contains(associationId)) { + detachSystemDataTransport(associationId); + } + + try { + final Transport transport = new Transport(associationId, in, out, flags); mTransports.put(associationId, transport); transport.start(); } catch (IOException e) { @@ -1931,16 +1993,22 @@ public final class CompanionDeviceManager { private final int mAssociationId; private final InputStream mRemoteIn; private final OutputStream mRemoteOut; + private final int mFlags; private InputStream mLocalIn; private OutputStream mLocalOut; private volatile boolean mStopped; - public Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) { + Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) { + this(associationId, remoteIn, remoteOut, 0); + } + + Transport(int associationId, InputStream remoteIn, OutputStream remoteOut, int flags) { mAssociationId = associationId; mRemoteIn = remoteIn; mRemoteOut = remoteOut; + mFlags = flags; } public void start() throws IOException { @@ -1957,7 +2025,7 @@ public final class CompanionDeviceManager { try { mService.attachSystemDataTransport(mContext.getOpPackageName(), - mContext.getUserId(), mAssociationId, remoteFd); + mContext.getUserId(), mAssociationId, remoteFd, mFlags); } catch (RemoteException e) { throw new IOException("Failed to configure transport", e); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index a2b7dd9c3d0e..787e8b65a736 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -113,7 +113,7 @@ interface ICompanionDeviceManager { in ISystemDataTransferCallback callback); @EnforcePermission("DELIVER_COMPANION_MESSAGES") - void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd); + void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd, int flags); @EnforcePermission("DELIVER_COMPANION_MESSAGES") void detachSystemDataTransport(String packageName, int userId, int associationId); diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 37f3f17ebe42..e6450606d450 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1643,6 +1643,19 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION = 327313645L; /** + * When the override is enabled, the activity receives configuration coupled with caption bar + * insets. Normally, caption bar insets are decoupled from configuration. + * + * <p>Override applies only if the activity targets SDK level 34 or earlier version. + * + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS = 388014743L; + + /** * Optional set of a certificates identifying apps that are allowed to embed this activity. From * the "knownActivityEmbeddingCerts" attribute. */ diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index 027eb9df1d9e..88fbdaddb3d2 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -137,4 +137,15 @@ flag { namespace: "resource_manager" description: "flag always meant to be false, for testing resource flagging within cts tests" bug: "377974898" -}
\ No newline at end of file +} + +flag { + name: "use_new_aconfig_storage" + is_exported: true + namespace: "resource_manager" + description: "Retrieve flag values from new Aconfig flag storage in AconfigFlags.java" + bug: "352348353" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index fded88212127..d8919160320a 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -641,6 +641,9 @@ public final class DisplayManager { * is triggered whenever the properties of a {@link android.view.Display}, such as size, * state, density are modified. * + * This event is not triggered for refresh rate changes as they can change very often. + * To monitor refresh rate changes, subscribe to {@link EVENT_TYPE_DISPLAY_REFRESH_RATE}. + * * @see #registerDisplayListener(DisplayListener, Handler, long) * */ @@ -839,6 +842,9 @@ public final class DisplayManager { * Registers a display listener to receive notifications about when * displays are added, removed or changed. * + * We encourage to use {@link #registerDisplayListener(Executor, long, DisplayListener)} + * instead to subscribe for explicit events of interest + * * @param listener The listener to register. * @param handler The handler on which the listener should be invoked, or null * if the listener should be invoked on the calling thread's looper. @@ -847,7 +853,9 @@ public final class DisplayManager { */ public void registerDisplayListener(DisplayListener listener, Handler handler) { registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED - | EVENT_TYPE_DISPLAY_CHANGED | EVENT_TYPE_DISPLAY_REMOVED); + | EVENT_TYPE_DISPLAY_CHANGED + | EVENT_TYPE_DISPLAY_REFRESH_RATE + | EVENT_TYPE_DISPLAY_REMOVED); } /** diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index b5715ed25bd9..339dbf2c2029 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -1766,29 +1766,23 @@ public final class DisplayManagerGlobal { } if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_CHANGED) != 0) { - // For backward compatibility, a client subscribing to - // DisplayManager.EVENT_FLAG_DISPLAY_CHANGED will be enrolled to both Basic and - // RR changes - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED; } - if ((eventFlags - & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) { + if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; } - if (Flags.displayListenerPerformanceImprovements()) { - if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) { - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; - } + if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + } + if (Flags.displayListenerPerformanceImprovements()) { if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_STATE) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_STATE; } } - return baseEventMask; } } diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 2d9d025b8d80..1a54f4df58fb 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -159,7 +159,7 @@ public class TestLooperManager { */ public void execute(Message message) { checkReleased(); - if (Looper.myLooper() == mLooper) { + if (mLooper.isCurrentThread()) { // This is being called from the thread it should be executed on, we can just dispatch. message.target.dispatchMessage(message); } else { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3cd7a00591ca..f1a9514107da 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13006,6 +13006,24 @@ public final class Settings { public static final String STYLUS_POINTER_ICON_ENABLED = "stylus_pointer_icon_enabled"; /** + * Toggle for whether to redact OTP notification while connected to wifi. Defaults to + * false/0. + * @hide + */ + @Readable + public static final String REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI = + "redact_otp_on_wifi"; + + /** + * Toggle for whether to immediately redact OTP notifications, or require the device to be + * locked for 10 minutes. Defaults to false/0 + * @hide + */ + @Readable + public static final String REDACT_OTP_NOTIFICATION_IMMEDIATELY = + "remove_otp_redaction_delay"; + + /** * These entries are considered common between the personal and the managed profile, * since the managed profile doesn't get to change them. */ diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 195896dc8edf..0e78bfdb5069 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -180,7 +180,7 @@ public final class InputEventConsistencyVerifier { final MotionEvent motionEvent = (MotionEvent)event; if (motionEvent.isTouchEvent()) { onTouchEvent(motionEvent, nestingLevel); - } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + } else if (motionEvent.isFromSource(InputDevice.SOURCE_TRACKBALL)) { onTrackballEvent(motionEvent, nestingLevel); } else { onGenericMotionEvent(motionEvent, nestingLevel); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 0d6f82773622..80b4f2caabbb 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -272,9 +272,9 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.ScreenCapture; import android.window.SurfaceSyncGroup; -import android.window.WindowContext; import android.window.WindowOnBackInvokedDispatcher; import android.window.WindowTokenClient; +import android.window.WindowTokenClientController; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -6614,12 +6614,15 @@ public final class ViewRootImpl implements ViewParent, } else { if (enableWindowContextResourcesUpdateOnConfigChange()) { // There is no activity callback - update resources for window token, if needed. - final WindowTokenClient windowTokenClient = getWindowTokenClient(); - if (windowTokenClient != null) { - windowTokenClient.onConfigurationChanged( + final IBinder windowContextToken = mContext.getWindowContextToken(); + if (windowContextToken instanceof WindowTokenClient) { + WindowTokenClientController.getInstance().onWindowConfigurationChanged( + windowContextToken, mLastReportedMergedConfiguration.getMergedConfiguration(), - newDisplayId == INVALID_DISPLAY ? mDisplay.getDisplayId() - : newDisplayId); + newDisplayId == INVALID_DISPLAY + ? mDisplay.getDisplayId() + : newDisplayId + ); } } updateConfiguration(newDisplayId); @@ -6627,11 +6630,6 @@ public final class ViewRootImpl implements ViewParent, mForceNextConfigUpdate = false; } - private WindowTokenClient getWindowTokenClient() { - if (!(mContext instanceof WindowContext)) return null; - return (WindowTokenClient) mContext.getWindowContextToken(); - } - /** * Update display and views if last applied merged configuration changed. * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise. diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0fb80422833c..56f0415b40cc 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -3778,8 +3778,32 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), - false /* fromIme */, statsToken); + synchronized (mH) { + Handler vh = rootView.getHandler(); + if (vh == null) { + // If the view doesn't have a handler, something has changed out from + // under us. + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + + if (vh.getLooper() != Looper.myLooper()) { + // The view is running on a different thread than our own, so + // we need to reschedule our work for over there. + if (DEBUG) { + Log.v(TAG, "Close current input: reschedule hide to view thread"); + } + final var viewRootImpl = mCurRootView; + vh.post(() -> viewRootImpl.getInsetsController().hide( + WindowInsets.Type.ime(), false /* fromIme */, statsToken)); + } else { + mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); + } + } } else { IInputMethodManagerGlobalInvoker.hideSoftInput( mClient, diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 785246074cee..1ce5df7cd137 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -55,6 +55,7 @@ public enum DesktopModeFlags { Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix, true), + ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( Flags::enableDesktopRecentsTransitionsCornersBugfix, false), diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java index 89327fe358f5..bc5ad50483ee 100644 --- a/core/java/android/window/TaskFragmentCreationParams.java +++ b/core/java/android/window/TaskFragmentCreationParams.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; +import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.ScreenOrientation; import android.graphics.Rect; import android.os.IBinder; @@ -112,12 +113,21 @@ public final class TaskFragmentCreationParams implements Parcelable { */ private final @ScreenOrientation int mOverrideOrientation; + /** + * {@link android.content.pm.ActivityInfo.Config} mask that specifies which + * configuration changes should trigger TaskFragment info change callbacks. + * + * @see android.content.pm.ActivityInfo.Config + */ + private final @ActivityInfo.Config int mConfigurationChangeMask; + private TaskFragmentCreationParams( @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect initialRelativeBounds, @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken, @Nullable IBinder pairedActivityToken, boolean allowTransitionWhenEmpty, - @ScreenOrientation int overrideOrientation) { + @ScreenOrientation int overrideOrientation, + @ActivityInfo.Config int configurationChangeMask) { if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) { throw new IllegalArgumentException("pairedPrimaryFragmentToken and" + " pairedActivityToken should not be set at the same time."); @@ -131,6 +141,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mPairedActivityToken = pairedActivityToken; mAllowTransitionWhenEmpty = allowTransitionWhenEmpty; mOverrideOrientation = overrideOrientation; + mConfigurationChangeMask = configurationChangeMask; } @NonNull @@ -186,6 +197,11 @@ public final class TaskFragmentCreationParams implements Parcelable { return mOverrideOrientation; } + /** @hide */ + public @ActivityInfo.Config int getConfigurationChangeMask() { + return mConfigurationChangeMask; + } + private TaskFragmentCreationParams(Parcel in) { mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in); mFragmentToken = in.readStrongBinder(); @@ -196,6 +212,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mPairedActivityToken = in.readStrongBinder(); mAllowTransitionWhenEmpty = in.readBoolean(); mOverrideOrientation = in.readInt(); + mConfigurationChangeMask = in.readInt(); } /** @hide */ @@ -210,6 +227,7 @@ public final class TaskFragmentCreationParams implements Parcelable { dest.writeStrongBinder(mPairedActivityToken); dest.writeBoolean(mAllowTransitionWhenEmpty); dest.writeInt(mOverrideOrientation); + dest.writeInt(mConfigurationChangeMask); } @NonNull @@ -238,6 +256,7 @@ public final class TaskFragmentCreationParams implements Parcelable { + " pairedActivityToken=" + mPairedActivityToken + " allowTransitionWhenEmpty=" + mAllowTransitionWhenEmpty + " overrideOrientation=" + mOverrideOrientation + + " configurationChangeMask=" + mConfigurationChangeMask + "}"; } @@ -275,6 +294,8 @@ public final class TaskFragmentCreationParams implements Parcelable { private @ScreenOrientation int mOverrideOrientation = SCREEN_ORIENTATION_UNSPECIFIED; + private @ActivityInfo.Config int mConfigurationChangeMask = 0; + public Builder(@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { mOrganizer = organizer; @@ -369,12 +390,30 @@ public final class TaskFragmentCreationParams implements Parcelable { return this; } + /** + * Sets {@link android.content.pm.ActivityInfo.Config} mask that specifies which + * configuration changes should trigger TaskFragment info change callbacks. + * + * Only system organizers are allowed to configure this value. This value is ignored for + * non-system organizers. + * + * @see android.content.pm.ActivityInfo.Config + * @hide + */ + @NonNull + public Builder setConfigurationChangeMask( + @ActivityInfo.Config int configurationChangeMask) { + mConfigurationChangeMask = configurationChangeMask; + return this; + } + /** Constructs the options to create TaskFragment with. */ @NonNull public TaskFragmentCreationParams build() { return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken, mInitialRelativeBounds, mWindowingMode, mPairedPrimaryFragmentToken, - mPairedActivityToken, mAllowTransitionWhenEmpty, mOverrideOrientation); + mPairedActivityToken, mAllowTransitionWhenEmpty, mOverrideOrientation, + mConfigurationChangeMask); } } } diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index f7bee619bc4b..5a544d3549a0 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -107,6 +107,7 @@ public class WindowTokenClient extends Binder { * @param newDisplayId the updated {@link android.view.Display} ID */ @MainThread + @VisibleForTesting(visibility = PACKAGE) public void onConfigurationChanged(Configuration newConfig, int newDisplayId) { onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */); } diff --git a/core/java/android/window/WindowTokenClientController.java b/core/java/android/window/WindowTokenClientController.java index fcd7dfbb1769..72278d927d74 100644 --- a/core/java/android/window/WindowTokenClientController.java +++ b/core/java/android/window/WindowTokenClientController.java @@ -25,7 +25,9 @@ import android.app.IApplicationThread; import android.app.servertransaction.WindowContextInfoChangeItem; import android.app.servertransaction.WindowContextWindowRemovalItem; import android.content.Context; +import android.content.res.Configuration; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.util.ArraySet; @@ -50,6 +52,7 @@ public class WindowTokenClientController { private final Object mLock = new Object(); private final IApplicationThread mAppThread = ActivityThread.currentActivityThread() .getApplicationThread(); + private final Handler mHandler = ActivityThread.currentActivityThread().getHandler(); /** Attached {@link WindowTokenClient}. */ @GuardedBy("mLock") @@ -257,6 +260,20 @@ public class WindowTokenClientController { } } + /** Propagates the configuration change to the client token. */ + public void onWindowConfigurationChanged(@NonNull IBinder clientToken, + @NonNull Configuration config, int displayId) { + final WindowTokenClient windowTokenClient = getWindowTokenClientIfAttached(clientToken); + if (windowTokenClient != null) { + // Let's make sure it's called on the main thread! + if (mHandler.getLooper().isCurrentThread()) { + windowTokenClient.onConfigurationChanged(config, displayId); + } else { + windowTokenClient.postOnConfigurationChanged(config, displayId); + } + } + } + @Nullable private WindowTokenClient getWindowTokenClientIfAttached(@NonNull IBinder clientToken) { if (!(clientToken instanceof WindowTokenClient windowTokenClient)) { diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 509e084e01e6..b805ac560b8d 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -695,4 +695,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "enable_desktop_close_shortcut_bugfix" + namespace: "lse_desktop_experience" + description: "Fix the window-close keyboard shortcut in Desktop Mode." + bug: "394599430" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/core/java/com/android/internal/app/IAppOpsCallback.aidl deleted file mode 100644 index 3a9525c03161..000000000000 --- a/core/java/com/android/internal/app/IAppOpsCallback.aidl +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.app; - -// This interface is also used by native code, so must -// be kept in sync with frameworks/native/libs/permission/include/binder/IAppOpsCallback.h -oneway interface IAppOpsCallback { - void opChanged(int op, int uid, String packageName, String persistentDeviceId); -} diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index 445dac7411da..21d000dc5224 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -16,6 +16,7 @@ package com.android.internal.pm.pkg.component; +import static android.provider.flags.Flags.newStoragePublicApi; import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE; import android.aconfig.DeviceProtos; @@ -27,6 +28,7 @@ import android.annotation.Nullable; import android.content.res.Flags; import android.os.Environment; import android.os.Process; +import android.os.flagging.AconfigPackage; import android.util.ArrayMap; import android.util.Slog; import android.util.Xml; @@ -43,6 +45,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * A class that manages a cache of all device feature flags and their default + override values. @@ -58,7 +61,8 @@ public class AconfigFlags { private static final String OVERRIDE_PREFIX = "device_config_overrides/"; private static final String STAGED_PREFIX = "staged/"; - private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>(); + private final Map<String, Boolean> mFlagValues = new ArrayMap<>(); + private final Map<String, AconfigPackage> mAconfigPackages = new ConcurrentHashMap<>(); public AconfigFlags() { if (!Flags.manifestFlagging()) { @@ -67,23 +71,33 @@ public class AconfigFlags { } return; } - final var defaultFlagProtoFiles = - (Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths() - : Arrays.asList(DeviceProtos.PATHS); - for (String fileName : defaultFlagProtoFiles) { - try (var inputStream = new FileInputStream(fileName)) { - loadAconfigDefaultValues(inputStream.readAllBytes()); - } catch (IOException e) { - Slog.e(LOG_TAG, "Failed to read Aconfig values from " + fileName, e); + + if (useNewStorage()) { + Slog.i(LOG_TAG, "Using new flag storage"); + } else { + Slog.i(LOG_TAG, "Using OLD proto flag storage"); + final var defaultFlagProtoFiles = + (Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths() + : Arrays.asList(DeviceProtos.PATHS); + for (String fileName : defaultFlagProtoFiles) { + try (var inputStream = new FileInputStream(fileName)) { + loadAconfigDefaultValues(inputStream.readAllBytes()); + } catch (IOException e) { + Slog.w(LOG_TAG, "Failed to read Aconfig values from " + fileName, e); + } + } + if (Process.myUid() == Process.SYSTEM_UID) { + // Server overrides are only accessible to the system, no need to even try loading + // them in user processes. + loadServerOverrides(); } - } - if (Process.myUid() == Process.SYSTEM_UID) { - // Server overrides are only accessible to the system, no need to even try loading them - // in user processes. - loadServerOverrides(); } } + private static boolean useNewStorage() { + return newStoragePublicApi() && Flags.useNewAconfigStorage(); + } + private void loadServerOverrides() { // Reading the proto files is enough for READ_ONLY flags but if it's a READ_WRITE flag // (which you can check with `flag.getPermission() == flag_permission.READ_WRITE`) then we @@ -200,7 +214,40 @@ public class AconfigFlags { */ @Nullable public Boolean getFlagValue(@NonNull String flagPackageAndName) { - Boolean value = mFlagValues.get(flagPackageAndName); + if (useNewStorage()) { + return getFlagValueFromNewStorage(flagPackageAndName); + } else { + Boolean value = mFlagValues.get(flagPackageAndName); + if (DEBUG) { + Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); + } + return value; + } + } + + private Boolean getFlagValueFromNewStorage(String flagPackageAndName) { + int index = flagPackageAndName.lastIndexOf('.'); + if (index < 0) { + Slog.e(LOG_TAG, "Unable to parse package name from " + flagPackageAndName); + return null; + } + String flagPackage = flagPackageAndName.substring(0, index); + String flagName = flagPackageAndName.substring(index + 1); + Boolean value = null; + AconfigPackage aconfigPackage = mAconfigPackages.computeIfAbsent(flagPackage, p -> { + try { + return AconfigPackage.load(p); + } catch (Exception e) { + Slog.e(LOG_TAG, "Failed to load aconfig package " + p, e); + return null; + } + }); + if (aconfigPackage != null) { + // Default value is false for when the flag is not found. + // Note: Unlike with the old storage, with AconfigPackage, we don't have a way to + // know if the flag is not found or if it's found but the value is false. + value = aconfigPackage.getBooleanFlagValue(flagName, false); + } if (DEBUG) { Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); } diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 05a33fe830e8..d8cf258e23ba 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -160,19 +160,21 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen Objects.requireNonNull(mConfigurationService, "A null ProtoLog Configuration Service was provided!"); - try { - var args = createConfigurationServiceRegisterClientArgs(); + mBackgroundLoggingService.execute(() -> { + try { + var args = createConfigurationServiceRegisterClientArgs(); - final var groupArgs = mLogGroups.values().stream() - .map(group -> new RegisterClientArgs - .GroupConfig(group.name(), group.isLogToLogcat())) - .toArray(RegisterClientArgs.GroupConfig[]::new); - args.setGroups(groupArgs); + final var groupArgs = mLogGroups.values().stream() + .map(group -> new RegisterClientArgs + .GroupConfig(group.name(), group.isLogToLogcat())) + .toArray(RegisterClientArgs.GroupConfig[]::new); + args.setGroups(groupArgs); - mConfigurationService.registerClient(this, args); - } catch (RemoteException e) { - throw new RuntimeException("Failed to register ProtoLog client"); - } + mConfigurationService.registerClient(this, args); + } catch (RemoteException e) { + throw new RuntimeException("Failed to register ProtoLog client"); + } + }); } /** diff --git a/core/java/com/android/internal/protolog/WmProtoLogGroups.java b/core/java/com/android/internal/protolog/WmProtoLogGroups.java index 4bd5d24b71e2..5edc2fbd4c8f 100644 --- a/core/java/com/android/internal/protolog/WmProtoLogGroups.java +++ b/core/java/com/android/internal/protolog/WmProtoLogGroups.java @@ -100,6 +100,8 @@ public enum WmProtoLogGroups implements IProtoLogGroup { WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), WM_DEBUG_EMBEDDED_WINDOWS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_PRESENTATION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 5e82772730b7..905d4dd547f3 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -83,7 +83,7 @@ public final class NotificationProgressBar extends ProgressBar implements /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; - private int mTrackerWidth; + private int mTrackerDrawWidth = 0; private int mTrackerPos; private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; @@ -157,7 +157,7 @@ public final class NotificationProgressBar extends ProgressBar implements } else { // TODO: b/372908709 - maybe don't rerun the entire calculation every time the // progress model is updated? For example, if the segments and parts aren't changed, - // there is no need to call `processAndConvertToViewParts` again. + // there is no need to call `processModelAndConvertToViewParts` again. final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); @@ -286,8 +286,11 @@ public final class NotificationProgressBar extends ProgressBar implements private void configureTrackerBounds() { // Reset the tracker draw matrix to null mTrackerDrawMatrix = null; + mTrackerDrawWidth = 0; - if (mTracker == null || mTrackerHeight <= 0) { + if (mTracker == null) return; + if (mTrackerHeight <= 0) { + mTrackerDrawWidth = mTracker.getIntrinsicWidth(); return; } @@ -306,14 +309,14 @@ public final class NotificationProgressBar extends ProgressBar implements if (dWidth > maxDWidth) { scale = (float) mTrackerHeight / (float) dHeight; dx = (maxDWidth * scale - dWidth * scale) * 0.5f; - mTrackerWidth = (int) (maxDWidth * scale); + mTrackerDrawWidth = (int) (maxDWidth * scale); } else if (dHeight > maxDHeight) { scale = (float) mTrackerHeight * 0.5f / (float) dWidth; dy = (maxDHeight * scale - dHeight * scale) * 0.5f; - mTrackerWidth = mTrackerHeight / 2; + mTrackerDrawWidth = mTrackerHeight / 2; } else { scale = (float) mTrackerHeight / (float) dHeight; - mTrackerWidth = (int) (dWidth * scale); + mTrackerDrawWidth = (int) (dWidth * scale); } mTrackerDrawMatrix.setScale(scale, scale); @@ -449,7 +452,8 @@ public final class NotificationProgressBar extends ProgressBar implements segSegGap, segPointGap, pointRadius, - mHasTrackerIcon + mHasTrackerIcon, + mTrackerDrawWidth ); final float segmentMinWidth = mNotificationProgressDrawable.getSegmentMinWidth(); @@ -465,7 +469,6 @@ public final class NotificationProgressBar extends ProgressBar implements segmentMinWidth, pointRadius, progressFraction, - width, isStyledByProgress, progressGap ); @@ -493,8 +496,8 @@ public final class NotificationProgressBar extends ProgressBar implements pointRadius, mHasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + mTrackerDrawWidth); } catch (NotEnoughWidthToFitAllPartsException ex) { Log.w(TAG, "Failed to stretch and rescale segments with single segment fallback", ex); @@ -522,8 +525,8 @@ public final class NotificationProgressBar extends ProgressBar implements pointRadius, mHasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + mTrackerDrawWidth); } catch (NotEnoughWidthToFitAllPartsException ex) { Log.w(TAG, "Failed to stretch and rescale segments with single segments and no points", @@ -537,16 +540,20 @@ public final class NotificationProgressBar extends ProgressBar implements mParts, mProgressDrawableParts, progressFraction, - width, isStyledByProgress, progressGap); } + // Extend the first and last segments to fill the entire width. + p.first.getFirst().setStart(0); + p.first.getLast().setEnd(width); + if (DEBUG) { Log.d(TAG, "Updating NotificationProgressDrawable parts"); } mNotificationProgressDrawable.setParts(p.first); - mAdjustedProgressFraction = p.second / width; + mAdjustedProgressFraction = + (p.second - mTrackerDrawWidth / 2F) / (width - mTrackerDrawWidth); } private void updateTrackerAndBarPos(int w, int h) { @@ -607,7 +614,7 @@ public final class NotificationProgressBar extends ProgressBar implements int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); - available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); + available -= mTrackerDrawWidth; final int trackerPos = (int) (progressFraction * available + 0.5f); @@ -672,7 +679,7 @@ public final class NotificationProgressBar extends ProgressBar implements canvas.translate(mPaddingLeft + mTrackerPos, mPaddingTop); if (mTrackerHeight > 0) { - canvas.clipRect(0, 0, mTrackerWidth, mTrackerHeight); + canvas.clipRect(0, 0, mTrackerDrawWidth, mTrackerHeight); } if (mTrackerDrawMatrix != null) { @@ -751,6 +758,7 @@ public final class NotificationProgressBar extends ProgressBar implements throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -758,6 +766,19 @@ public final class NotificationProgressBar extends ProgressBar implements } } + // There should be no points at start or end. If there are, drop them with a warning. + points.removeIf(point -> { + final int pos = point.getPosition(); + if (pos == 0) { + Log.w(TAG, "Dropping point at start"); + return true; + } else if (pos == progressMax) { + Log.w(TAG, "Dropping point at end"); + return true; + } + return false; + }); + final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap( segments); final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( @@ -891,12 +912,14 @@ public final class NotificationProgressBar extends ProgressBar implements float segSegGap, float segPointGap, float pointRadius, - boolean hasTrackerIcon - ) { + boolean hasTrackerIcon, + int trackerDrawWidth) { List<DrawablePart> drawableParts = new ArrayList<>(); - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) 0; + float available = totalWidth - trackerDrawWidth; + // Generally, we will start the first segment at (x+trackerDrawWidth/2, y) and end the last + // segment at (x+w-trackerDrawWidth/2, y) + float x = trackerDrawWidth / 2F; final int nParts = parts.size(); for (int iPart = 0; iPart < nParts; iPart++) { @@ -904,15 +927,14 @@ public final class NotificationProgressBar extends ProgressBar implements final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; + final float segWidth = segment.mFraction * available; // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, - iPart == 1); + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap); final float start = x + startOffset; // Retract the end position to account for the padding and a point immediately // after. final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - segSegGap, iPart == nParts - 2, hasTrackerIcon); + segSegGap, hasTrackerIcon); final float end = x + segWidth - endOffset; drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); @@ -927,16 +949,6 @@ public final class NotificationProgressBar extends ProgressBar implements final float pointWidth = 2 * pointRadius; float start = x - pointRadius; float end = x + pointRadius; - // Only shift the points right at the start/end. - // For the points close to the start/end, the segment minimum width requirement - // would take care of shifting them to be within the bounds. - if (iPart == 0) { - start = 0; - end = pointWidth; - } else if (iPart == nParts - 1) { - start = totalWidth - pointWidth; - end = totalWidth; - } drawableParts.add(new DrawablePoint(start, end, point.mColor)); } @@ -945,16 +957,13 @@ public final class NotificationProgressBar extends ProgressBar implements return drawableParts; } - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - boolean isSecondPart) { + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap) { if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = isSecondPart ? pointRadius : 0; - return pointOffset + pointRadius + segPointGap; + return pointRadius + segPointGap; } private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, float segSegGap, boolean isSecondToLastPart, - boolean hasTrackerIcon) { + float segPointGap, float segSegGap, boolean hasTrackerIcon) { if (nextPart == null) return 0F; if (nextPart instanceof Segment nextSeg) { if (!seg.mFaded && nextSeg.mFaded) { @@ -964,8 +973,7 @@ public final class NotificationProgressBar extends ProgressBar implements return segSegGap; } - final float pointOffset = isSecondToLastPart ? pointRadius : 0; - return segPointGap + pointRadius + pointOffset; + return segPointGap + pointRadius; } /** @@ -980,7 +988,6 @@ public final class NotificationProgressBar extends ProgressBar implements float segmentMinWidth, float pointRadius, float progressFraction, - float totalWidth, boolean isStyledByProgress, float progressGap ) throws NotEnoughWidthToFitAllPartsException { @@ -1003,7 +1010,6 @@ public final class NotificationProgressBar extends ProgressBar implements parts, drawableParts, progressFraction, - totalWidth, isStyledByProgress, progressGap); } @@ -1056,7 +1062,6 @@ public final class NotificationProgressBar extends ProgressBar implements parts, drawableParts, progressFraction, - totalWidth, isStyledByProgress, progressGap); } @@ -1071,11 +1076,12 @@ public final class NotificationProgressBar extends ProgressBar implements List<Part> parts, List<DrawablePart> drawableParts, float progressFraction, - float totalWidth, boolean isStyledByProgress, float progressGap ) { - if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + if (progressFraction == 1) { + return new Pair<>(drawableParts, drawableParts.getLast().getEnd()); + } int iPartFirstSegmentToStyle = -1; int iPartSegmentToSplit = -1; @@ -1162,14 +1168,15 @@ public final class NotificationProgressBar extends ProgressBar implements float pointRadius, boolean hasTrackerIcon, float segmentMinWidth, - boolean isStyledByProgress + boolean isStyledByProgress, + int trackerDrawWidth ) throws NotEnoughWidthToFitAllPartsException { List<Part> parts = processModelAndConvertToViewParts(segments, points, progress, progressMax); List<DrawablePart> drawableParts = processPartsAndConvertToDrawableParts(parts, totalWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon); + segSegGap, segPointGap, pointRadius, hasTrackerIcon, trackerDrawWidth); return maybeStretchAndRescaleSegments(parts, drawableParts, segmentMinWidth, pointRadius, - getProgressFraction(progressMax, progress), totalWidth, isStyledByProgress, + getProgressFraction(progressMax, progress), isStyledByProgress, hasTrackerIcon ? 0F : segSegGap); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c9f4cdc8e3ce..51049889ecd6 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -9385,6 +9385,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.security.UpdateCertificateRevocationStatusJobService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d7ffcc54562c..17acf9aed278 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7352,6 +7352,21 @@ default on this device--> <string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" /> + <!-- Preference name of bugreport--> + <string name="prefs_bugreport" translatable="false">bugreports</string> + + <!-- key value of warning state stored in bugreport preference--> + <string name="key_warning_state" translatable="false">warning-state</string> + + <!-- Bugreport warning dialog state unknown--> + <integer name="bugreport_state_unknown">0</integer> + + <!-- Bugreport warning dialog state shows the warning dialog--> + <integer name="bugreport_state_show">1</integer> + + <!-- Bugreport warning dialog state skips the warning dialog--> + <integer name="bugreport_state_hide">2</integer> + <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This config enables OEMs to support its usage across tasks.--> <bool name="config_enableCrossTaskScaleUpAnimation">false</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6701e63c4f90..cc2897a2779e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5906,6 +5906,12 @@ <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_title" /> <java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_text" /> + <java-symbol type="string" name="prefs_bugreport" /> + <java-symbol type="string" name="key_warning_state" /> + <java-symbol type="integer" name="bugreport_state_unknown" /> + <java-symbol type="integer" name="bugreport_state_show" /> + <java-symbol type="integer" name="bugreport_state_hide" /> + <!-- Enable OEMs to support scale up anim across tasks.--> <java-symbol type="bool" name="config_enableCrossTaskScaleUpAnimation" /> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index ca6ad6fae46e..f89e4416ce78 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2532,6 +2532,46 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_addProgressPoint_dropsZeroPoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Points should not be a negative integer. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(0)); + + // THEN + assertThat(progressStyle.getProgressPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoint_dropsZeroPoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Points should not be a negative integer. + progressStyle + .setProgressPoints(List.of(new Notification.ProgressStyle.Point(0))); + + // THEN + assertThat(progressStyle.getProgressPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_ignoresPointsAtMax() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.addProgressSegment(new Notification.ProgressStyle.Segment(100)); + // Points should not larger than progress maximum. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(100)); + + // THEN + assertThat(progressStyle.createProgressModel(Color.BLUE, Color.RED).getPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2573,14 +2613,14 @@ public class NotificationTest { // THEN assertThat(progressStyle.createProgressModel(defaultProgressColor, backgroundColor) .getPoints()).isEqualTo( - List.of(new Notification.ProgressStyle.Point(0) - .setColor(expectedProgressColor), - new Notification.ProgressStyle.Point(20) + List.of(new Notification.ProgressStyle.Point(20) .setColor(expectedProgressColor), new Notification.ProgressStyle.Point(50) .setColor(expectedProgressColor), new Notification.ProgressStyle.Point(70) - .setColor(expectedProgressColor) + .setColor(expectedProgressColor), + new Notification.ProgressStyle.Point(80) + .setColor(expectedProgressColor) ) ); } diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 8fa510381060..dc2f0a69375d 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -307,8 +307,10 @@ public class DisplayManagerGlobalTest { assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, mDisplayManagerGlobal .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_ADDED, 0)); - assertEquals(DISPLAY_CHANGE_EVENTS, mDisplayManagerGlobal - .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, 0)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, + 0)); assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, mDisplayManagerGlobal.mapFiltersToInternalEventFlag( DisplayManager.EVENT_TYPE_DISPLAY_REMOVED, 0)); diff --git a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java index 84ff40f0dcf0..116dc124c902 100644 --- a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java +++ b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java @@ -266,4 +266,25 @@ public class WindowTokenClientControllerTest { verify(mWindowTokenClient).onWindowTokenRemoved(); } + + @Test + public void testOnWindowConfigurationChanged_propagatedToCorrectToken() throws RemoteException { + doReturn(mWindowContextInfo).when(mWindowManagerService) + .attachWindowContextToDisplayContent(any(), any(), anyInt()); + + mController.onWindowConfigurationChanged(mWindowTokenClient, mConfiguration, + DEFAULT_DISPLAY + 1); + + // Not propagated before attaching + verify(mWindowTokenClient, never()).onConfigurationChanged(mConfiguration, + DEFAULT_DISPLAY + 1); + + assertTrue(mController.attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY)); + + mController.onWindowConfigurationChanged(mWindowTokenClient, mConfiguration, + DEFAULT_DISPLAY + 1); + + // Now that's attached, propagating it. + verify(mWindowTokenClient).postOnConfigurationChanged(mConfiguration, DEFAULT_DISPLAY + 1); + } } diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index 9baa31faea08..282886af9ef8 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -121,18 +121,20 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, Color.RED))); + List.of(new DrawableSegment(10, 310, Color.RED))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -141,14 +143,14 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedRed = 0x80FF0000; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, fadedRed, true))); + List.of(new DrawableSegment(10, 310, fadedRed, true))); - assertThat(p.second).isEqualTo(0); + assertThat(p.second).isEqualTo(10); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -168,18 +170,20 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, Color.RED))); + List.of(new DrawableSegment(10, 310, Color.RED))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -188,9 +192,9 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(p.second).isEqualTo(300); + assertThat(p.second).isEqualTo(310); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -219,6 +223,42 @@ public class NotificationProgressBarTest { progressMax); } + @Test + public void processAndConvertToParts_pointPositionIsZero() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.RED)); + int progress = 50; + int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments, + points, progress, progressMax); + + // Point at the start is dropped. + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + } + + @Test + public void processAndConvertToParts_pointPositionAtMax() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(100).setColor(Color.RED)); + int progress = 50; + int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments, + points, progress, progressMax); + + // Point at the end is dropped. + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + } + @Test(expected = IllegalArgumentException.class) public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); @@ -249,18 +289,20 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 300, Color.BLUE))); + List.of(new DrawableSegment(10, 310, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -269,15 +311,15 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedBlue = 0x800000FF; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 180, Color.BLUE), - new DrawableSegment(180, 300, fadedBlue, true))); + List.of(new DrawableSegment(10, 190, Color.BLUE), + new DrawableSegment(190, 310, fadedBlue, true))); - assertThat(p.second).isEqualTo(180); + assertThat(p.second).isEqualTo(190); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -299,19 +341,21 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 146, Color.RED), - new DrawableSegment(150, 300, Color.GREEN))); + List.of(new DrawableSegment(10, 156, Color.RED), + new DrawableSegment(160, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -319,15 +363,15 @@ public class NotificationProgressBarTest { boolean isStyledByProgress = true; Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; - expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), - new DrawableSegment(150, 180, Color.GREEN), - new DrawableSegment(180, 300, fadedGreen, true))); + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(10, 156, Color.RED), + new DrawableSegment(160, 190, Color.GREEN), + new DrawableSegment(190, 310, fadedGreen, true))); - assertThat(p.second).isEqualTo(180); + assertThat(p.second).isEqualTo(190); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -353,10 +397,12 @@ public class NotificationProgressBarTest { float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = false; + int trackerDrawWidth = 0; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( List.of(new DrawableSegment(0, 146, Color.RED), @@ -368,7 +414,7 @@ public class NotificationProgressBarTest { boolean isStyledByProgress = true; Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; @@ -409,26 +455,28 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 35, Color.BLUE), - new DrawablePoint(39, 51, Color.RED), - new DrawableSegment(55, 65, Color.BLUE), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 170, Color.BLUE), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 215, Color.BLUE), - new DrawablePoint(219, 231, Color.YELLOW), - new DrawableSegment(235, 300, Color.BLUE))); + List.of(new DrawableSegment(10, 45, Color.BLUE), + new DrawablePoint(49, 61, Color.RED), + new DrawableSegment(65, 75, Color.BLUE), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 180, Color.BLUE), + new DrawablePoint(184, 196, Color.BLUE), + new DrawableSegment(200, 225, Color.BLUE), + new DrawablePoint(229, 241, Color.YELLOW), + new DrawableSegment(245, 310, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -437,23 +485,23 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedBlue = 0x800000FF; int fadedYellow = 0x80FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), - new DrawablePoint(38.219177F, 50.219177F, Color.RED), - new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), - new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), - new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), - new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), - new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), - new DrawablePoint(221.0137F, 233.0137F, fadedYellow), - new DrawableSegment(237.0137F, 300F, fadedBlue, true))); - - assertThat(p.second).isEqualTo(182.38356F); + List.of(new DrawableSegment(10, 44.219177F, Color.BLUE), + new DrawablePoint(48.219177F, 60.219177F, Color.RED), + new DrawableSegment(64.219177F, 80.21918F, Color.BLUE), + new DrawablePoint(84.21918F, 96.21918F, Color.BLUE), + new DrawableSegment(100.21918F, 182.38356F, Color.BLUE), + new DrawablePoint(186.38356F, 198.38356F, Color.BLUE), + new DrawableSegment(202.38356F, 227.0137F, fadedBlue, true), + new DrawablePoint(231.0137F, 243.0137F, fadedYellow), + new DrawableSegment(247.0137F, 310F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(192.38356F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -488,102 +536,29 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; - float segSegGap = 4; - float segPointGap = 4; - float pointRadius = 6; - boolean hasTrackerIcon = true; - List<DrawablePart> drawableParts = - NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - - List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), - new DrawableSegment(55, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 215, Color.GREEN), - new DrawablePoint(219, 231, Color.YELLOW), - new DrawableSegment(235, 300, Color.GREEN))); - - assertThat(drawableParts).isEqualTo(expectedDrawableParts); - - float segmentMinWidth = 16; - boolean isStyledByProgress = true; - - Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( - parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - - // Colors with 50% opacity - int fadedGreen = 0x8000FF00; - int fadedYellow = 0x80FFFF00; - expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.095238F, Color.RED), - new DrawablePoint(38.095238F, 50.095238F, Color.RED), - new DrawableSegment(54.095238F, 70.09524F, Color.RED), - new DrawablePoint(74.09524F, 86.09524F, Color.BLUE), - new DrawableSegment(90.09524F, 148.9524F, Color.RED), - new DrawableSegment(152.95238F, 172.7619F, Color.GREEN), - new DrawablePoint(176.7619F, 188.7619F, Color.BLUE), - new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true), - new DrawablePoint(221.33333F, 233.33333F, fadedYellow), - new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true))); - - assertThat(p.second).isEqualTo(182.7619F); - assertThat(p.first).isEqualTo(expectedDrawableParts); - } - - @Test - public void processAndConvertToParts_multipleSegmentsWithPointsAtStartAndEnd() - throws NotEnoughWidthToFitAllPartsException { - List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); - segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); - List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.RED)); - points.add(new ProgressStyle.Point(25).setColor(Color.BLUE)); - points.add(new ProgressStyle.Point(60).setColor(Color.BLUE)); - points.add(new ProgressStyle.Point(100).setColor(Color.YELLOW)); - int progress = 60; - int progressMax = 100; - - List<Part> parts = NotificationProgressBar.processModelAndConvertToViewParts(segments, - points, progress, progressMax); - - List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.RED), - new Segment(0.25f, Color.RED), - new Point(Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Point(Color.BLUE), - new Segment(0.4f, Color.GREEN), - new Point(Color.YELLOW))); - - assertThat(parts).isEqualTo(expectedParts); - - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.RED), - new DrawableSegment(16, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 284, Color.GREEN), - new DrawablePoint(288, 300, Color.YELLOW))); + List.of(new DrawableSegment(10, 45, Color.RED), + new DrawablePoint(49, 61, Color.RED), + new DrawableSegment(65, 75, Color.RED), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 156, Color.RED), + new DrawableSegment(160, 180, Color.GREEN), + new DrawablePoint(184, 196, Color.BLUE), + new DrawableSegment(200, 225, Color.GREEN), + new DrawablePoint(229, 241, Color.YELLOW), + new DrawableSegment(245, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -592,22 +567,24 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; int fadedYellow = 0x80FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.RED), - new DrawableSegment(16, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 284, fadedGreen, true), - new DrawablePoint(288, 300, fadedYellow))); - - assertThat(p.second).isEqualTo(180); + List.of(new DrawableSegment(10, 44.095238F, Color.RED), + new DrawablePoint(48.095238F, 60.095238F, Color.RED), + new DrawableSegment(64.095238F, 80.09524F, Color.RED), + new DrawablePoint(84.09524F, 96.09524F, Color.BLUE), + new DrawableSegment(100.09524F, 158.9524F, Color.RED), + new DrawableSegment(162.95238F, 182.7619F, Color.GREEN), + new DrawablePoint(186.7619F, 198.7619F, Color.BLUE), + new DrawableSegment(202.7619F, 227.33333F, fadedGreen, true), + new DrawablePoint(231.33333F, 243.33333F, fadedYellow), + new DrawableSegment(247.33333F, 309.99997F, fadedGreen, true))); + + assertThat(p.second).isEqualTo(192.7619F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -644,27 +621,29 @@ public class NotificationProgressBarTest { assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, -7, Color.RED), - new DrawablePoint(-3, 9, Color.RED), - new DrawableSegment(13, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 170, Color.GREEN), - new DrawablePoint(174, 186, Color.BLUE), - new DrawableSegment(190, 287, Color.GREEN), - new DrawablePoint(291, 303, Color.YELLOW), - new DrawableSegment(307, 300, Color.GREEN))); + List.of(new DrawableSegment(10, 3, Color.RED), + new DrawablePoint(7, 19, Color.RED), + new DrawableSegment(23, 75, Color.RED), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 156, Color.RED), + new DrawableSegment(160, 180, Color.GREEN), + new DrawablePoint(184, 196, Color.BLUE), + new DrawableSegment(200, 297, Color.GREEN), + new DrawablePoint(301, 313, Color.YELLOW), + new DrawableSegment(317, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -673,24 +652,24 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 50% opacity int fadedGreen = 0x8000FF00; int fadedYellow = 0x80FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 16, Color.RED), - new DrawablePoint(20, 32, Color.RED), - new DrawableSegment(36, 78.02409F, Color.RED), - new DrawablePoint(82.02409F, 94.02409F, Color.BLUE), - new DrawableSegment(98.02409F, 146.55421F, Color.RED), - new DrawableSegment(150.55421F, 169.44579F, Color.GREEN), - new DrawablePoint(173.44579F, 185.44579F, Color.BLUE), - new DrawableSegment(189.44579F, 264, fadedGreen, true), - new DrawablePoint(268, 280, fadedYellow), - new DrawableSegment(284, 300, fadedGreen, true))); - - assertThat(p.second).isEqualTo(179.44579F); + List.of(new DrawableSegment(10, 26, Color.RED), + new DrawablePoint(30, 42, Color.RED), + new DrawableSegment(46, 88.02409F, Color.RED), + new DrawablePoint(92.02409F, 104.02409F, Color.BLUE), + new DrawableSegment(108.02409F, 156.55421F, Color.RED), + new DrawableSegment(160.55421F, 179.44579F, Color.GREEN), + new DrawablePoint(183.44579F, 195.44579F, Color.BLUE), + new DrawableSegment(199.44579F, 274, fadedGreen, true), + new DrawablePoint(278, 290, fadedYellow), + new DrawableSegment(294, 310, fadedGreen, true))); + + assertThat(p.second).isEqualTo(189.44579F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -711,31 +690,38 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), - new Segment(0.10f, Color.RED), new Point(Color.BLUE), - new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), - new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + List.of(new Segment(0.15f, Color.RED), + new Point(Color.RED), + new Segment(0.10f, Color.RED), + new Point(Color.BLUE), + new Segment(0.25f, Color.RED), + new Segment(0.25f, Color.GREEN), + new Point(Color.YELLOW), + new Segment(0.25f, Color.GREEN))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), - new DrawableSegment(55, 65, Color.RED), - new DrawablePoint(69, 81, Color.BLUE), - new DrawableSegment(85, 146, Color.RED), - new DrawableSegment(150, 215, Color.GREEN), - new DrawablePoint(219, 231, Color.YELLOW), - new DrawableSegment(235, 300, Color.GREEN))); + List.of(new DrawableSegment(10, 45, Color.RED), + new DrawablePoint(49, 61, Color.RED), + new DrawableSegment(65, 75, Color.RED), + new DrawablePoint(79, 91, Color.BLUE), + new DrawableSegment(95, 156, Color.RED), + new DrawableSegment(160, 225, Color.GREEN), + new DrawablePoint(229, 241, Color.YELLOW), + new DrawableSegment(245, 310, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -744,34 +730,34 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.296295F, Color.RED), - new DrawablePoint(38.296295F, 50.296295F, Color.RED), - new DrawableSegment(54.296295F, 70.296295F, Color.RED), - new DrawablePoint(74.296295F, 86.296295F, Color.BLUE), - new DrawableSegment(90.296295F, 149.62962F, Color.RED), - new DrawableSegment(153.62962F, 216.8148F, Color.GREEN), - new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW), - new DrawableSegment(236.81482F, 300, Color.GREEN))); - - assertThat(p.second).isEqualTo(182.9037F); + List.of(new DrawableSegment(10, 44.296295F, Color.RED), + new DrawablePoint(48.296295F, 60.296295F, Color.RED), + new DrawableSegment(64.296295F, 80.296295F, Color.RED), + new DrawablePoint(84.296295F, 96.296295F, Color.BLUE), + new DrawableSegment(100.296295F, 159.62962F, Color.RED), + new DrawableSegment(163.62962F, 226.8148F, Color.GREEN), + new DrawablePoint(230.81482F, 242.81482F, Color.YELLOW), + new DrawableSegment(246.81482F, 310, Color.GREEN))); + + assertThat(p.second).isEqualTo(192.9037F); assertThat(p.first).isEqualTo(expectedDrawableParts); } - // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // The only difference from the `segmentWidthAtMin` test below is the longer // segmentMinWidth (= 16dp). @Test - public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() + public void maybeStretchAndRescaleSegments_segmentWidthBelowMin() throws NotEnoughWidthToFitAllPartsException { List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(200).setColor(Color.BLUE)); int progress = 1000; int progressMax = 1000; @@ -779,28 +765,32 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), - new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + List.of(new Segment(0.2f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.3f, Color.BLUE), new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 200; + float drawableWidth = 220; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 16, Color.BLUE), - new DrawableSegment(20, 56, Color.BLUE), - new DrawableSegment(60, 116, Color.BLUE), - new DrawableSegment(120, 200, Color.BLUE))); + List.of(new DrawableSegment(10, 40, Color.BLUE), + new DrawablePoint(44, 56, Color.BLUE), + new DrawableSegment(60, 66, Color.BLUE), + new DrawableSegment(70, 126, Color.BLUE), + new DrawableSegment(130, 210, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -809,30 +799,31 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 32, Color.BLUE), - new DrawableSegment(36, 69.41936F, Color.BLUE), - new DrawableSegment(73.41936F, 124.25807F, Color.BLUE), - new DrawableSegment(128.25807F, 200, Color.BLUE))); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(10, 38.81356F, Color.BLUE), + new DrawablePoint(42.81356F, 54.81356F, Color.BLUE), + new DrawableSegment(58.81356F, 74.81356F, Color.BLUE), + new DrawableSegment(78.81356F, 131.42374F, Color.BLUE), + new DrawableSegment(135.42374F, 210, Color.BLUE))); - assertThat(p.second).isEqualTo(200); + assertThat(p.second).isEqualTo(210); assertThat(p.first).isEqualTo(expectedDrawableParts); } - // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // The only difference from the `segmentWidthBelowMin` test above is the shorter // segmentMinWidth (= 10dp). @Test - public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() + public void maybeStretchAndRescaleSegments_segmentWidthAtMin() throws NotEnoughWidthToFitAllPartsException { List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(200).setColor(Color.BLUE)); int progress = 1000; int progressMax = 1000; @@ -840,28 +831,32 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), - new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + List.of(new Segment(0.2f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.3f, Color.BLUE), new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 200; + float drawableWidth = 220; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 16, Color.BLUE), - new DrawableSegment(20, 56, Color.BLUE), - new DrawableSegment(60, 116, Color.BLUE), - new DrawableSegment(120, 200, Color.BLUE))); + List.of(new DrawableSegment(10, 40, Color.BLUE), + new DrawablePoint(44, 56, Color.BLUE), + new DrawableSegment(60, 66, Color.BLUE), + new DrawableSegment(70, 126, Color.BLUE), + new DrawableSegment(130, 210, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -870,15 +865,16 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 26, Color.BLUE), - new DrawableSegment(30, 64.169014F, Color.BLUE), - new DrawableSegment(68.169014F, 120.92958F, Color.BLUE), - new DrawableSegment(124.92958F, 200, Color.BLUE))); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(10, 39.411766F, Color.BLUE), + new DrawablePoint(43.411766F, 55.411766F, Color.BLUE), + new DrawableSegment(59.411766F, 69.411766F, Color.BLUE), + new DrawableSegment(73.411766F, 128.05884F, Color.BLUE), + new DrawableSegment(132.05882F, 210, Color.BLUE))); - assertThat(p.second).isEqualTo(200); + assertThat(p.second).isEqualTo(210); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -886,12 +882,12 @@ public class NotificationProgressBarTest { public void maybeStretchAndRescaleSegments_noStretchingNecessary() throws NotEnoughWidthToFitAllPartsException { List<ProgressStyle.Segment> segments = new ArrayList<>(); - segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(100).setColor(Color.BLUE)); int progress = 1000; int progressMax = 1000; @@ -899,28 +895,32 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE), - new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE), + List.of(new Segment(0.1f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.3f, Color.BLUE), new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); - float drawableWidth = 200; + float drawableWidth = 220; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawablePoint(0, 12, Color.BLUE), - new DrawableSegment(16, 36, Color.BLUE), - new DrawableSegment(40, 56, Color.BLUE), - new DrawableSegment(60, 116, Color.BLUE), - new DrawableSegment(120, 200, Color.BLUE))); + List.of(new DrawableSegment(10, 20, Color.BLUE), + new DrawablePoint(24, 36, Color.BLUE), + new DrawableSegment(40, 66, Color.BLUE), + new DrawableSegment(70, 126, Color.BLUE), + new DrawableSegment(130, 210, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); @@ -929,9 +929,9 @@ public class NotificationProgressBarTest { Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(p.second).isEqualTo(200); + assertThat(p.second).isEqualTo(210); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -951,10 +951,10 @@ public class NotificationProgressBarTest { segments.add(new ProgressStyle.Segment(10).setColor(Color.GREEN)); segments.add(new ProgressStyle.Segment(10).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); - points.add(new ProgressStyle.Point(0).setColor(orange)); points.add(new ProgressStyle.Point(1).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(10).setColor(orange)); points.add(new ProgressStyle.Point(55).setColor(Color.BLUE)); - points.add(new ProgressStyle.Point(100).setColor(orange)); + points.add(new ProgressStyle.Point(90).setColor(orange)); int progress = 50; int progressMax = 100; @@ -962,10 +962,10 @@ public class NotificationProgressBarTest { points, progress, progressMax); List<Part> expectedParts = new ArrayList<>( - List.of(new Point(orange), - new Segment(0.01f, orange), + List.of(new Segment(0.01f, orange), new Point(Color.BLUE), new Segment(0.09f, orange), + new Point(orange), new Segment(0.1f, Color.YELLOW), new Segment(0.1f, Color.BLUE), new Segment(0.1f, Color.GREEN), @@ -976,21 +976,23 @@ public class NotificationProgressBarTest { new Segment(0.1f, Color.YELLOW), new Segment(0.1f, Color.BLUE), new Segment(0.1f, Color.GREEN), - new Segment(0.1f, Color.RED), - new Point(orange))); + new Point(orange), + new Segment(0.1f, Color.RED))); assertThat(parts).isEqualTo(expectedParts); // For the list of ProgressStyle.Part used in this test, 300 is the minimum width. - float drawableWidth = 299; + float drawableWidth = 319; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; List<DrawablePart> drawableParts = NotificationProgressBar.processPartsAndConvertToDrawableParts( - parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon, + trackerDrawWidth); // Skips the validation of the intermediate list of DrawableParts. @@ -999,7 +1001,7 @@ public class NotificationProgressBarTest { NotificationProgressBar.maybeStretchAndRescaleSegments( parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, - 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); } @Test @@ -1015,11 +1017,12 @@ public class NotificationProgressBarTest { int progress = 60; int progressMax = 100; - float drawableWidth = 300; + float drawableWidth = 320; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; float segmentMinWidth = 16; boolean isStyledByProgress = true; @@ -1036,24 +1039,24 @@ public class NotificationProgressBarTest { pointRadius, hasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + trackerDrawWidth); // Colors with 50% opacity int fadedBlue = 0x800000FF; int fadedYellow = 0x80FFFF00; List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), - new DrawablePoint(38.219177F, 50.219177F, Color.RED), - new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), - new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), - new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), - new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), - new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), - new DrawablePoint(221.0137F, 233.0137F, fadedYellow), - new DrawableSegment(237.0137F, 300F, fadedBlue, true))); - - assertThat(p.second).isEqualTo(182.38356F); + List.of(new DrawableSegment(10, 44.219177F, Color.BLUE), + new DrawablePoint(48.219177F, 60.219177F, Color.RED), + new DrawableSegment(64.219177F, 80.21918F, Color.BLUE), + new DrawablePoint(84.21918F, 96.21918F, Color.BLUE), + new DrawableSegment(100.21918F, 182.38356F, Color.BLUE), + new DrawablePoint(186.38356F, 198.38356F, Color.BLUE), + new DrawableSegment(202.38356F, 227.0137F, fadedBlue, true), + new DrawablePoint(231.0137F, 243.0137F, fadedYellow), + new DrawableSegment(247.0137F, 310F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(192.38356F); assertThat(p.first).isEqualTo(expectedDrawableParts); } @@ -1065,11 +1068,12 @@ public class NotificationProgressBarTest { int progress = 60; int progressMax = 100; - float drawableWidth = 100; + float drawableWidth = 120; float segSegGap = 4; float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; + int trackerDrawWidth = 20; float segmentMinWidth = 16; boolean isStyledByProgress = true; @@ -1086,16 +1090,16 @@ public class NotificationProgressBarTest { pointRadius, hasTrackerIcon, segmentMinWidth, - isStyledByProgress - ); + isStyledByProgress, + trackerDrawWidth); - // Colors with 50%f opacity + // Colors with 50% opacity int fadedBlue = 0x800000FF; List<DrawablePart> expectedDrawableParts = new ArrayList<>( - List.of(new DrawableSegment(0, 60.000004F, Color.BLUE), - new DrawableSegment(60.000004F, 100, fadedBlue, true))); + List.of(new DrawableSegment(10, 70F, Color.BLUE), + new DrawableSegment(70F, 110, fadedBlue, true))); - assertThat(p.second).isWithin(1e-5f).of(60); + assertThat(p.second).isEqualTo(70); assertThat(p.first).isEqualTo(expectedDrawableParts); } } diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java index e1f5b1c2e4a4..140d268e855b 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java @@ -90,7 +90,7 @@ public class NotificationProgressModelTest { new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW), new Notification.ProgressStyle.Segment(50).setColor(Color.LTGRAY)); final List<Notification.ProgressStyle.Point> points = List.of( - new Notification.ProgressStyle.Point(0).setColor(Color.RED), + new Notification.ProgressStyle.Point(1).setColor(Color.RED), new Notification.ProgressStyle.Point(20).setColor(Color.BLUE)); final NotificationProgressModel savedModel = new NotificationProgressModel(segments, points, @@ -121,7 +121,7 @@ public class NotificationProgressModelTest { new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW), new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW)); final List<Notification.ProgressStyle.Point> points = List.of( - new Notification.ProgressStyle.Point(0).setColor(Color.RED), + new Notification.ProgressStyle.Point(1).setColor(Color.RED), new Notification.ProgressStyle.Point(20).setColor(Color.BLUE)); final NotificationProgressModel savedModel = new NotificationProgressModel(segments, points, diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 13d0169c47c5..a08f88a5b937 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -177,3 +177,10 @@ flag { description: "Factor task-view state tracking out of taskviewtransitions" bug: "384976265" } + +flag { + name: "enable_bubble_bar_on_phones" + namespace: "multitasking" + description: "Try out bubble bar on phones" + bug: "394869612" +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt index bce6c5999a75..a32ec221e08a 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt @@ -61,7 +61,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.Optional @@ -133,7 +132,7 @@ class BubbleControllerBubbleBarTest { mainExecutor, bgExecutor, ) - bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java)) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) shellInit.init() diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 88bfeb21bb74..e865111e59dc 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -50,10 +50,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.verify import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit @@ -635,7 +635,7 @@ class BubbleStackViewTest { @Test fun removeFromWindow_stopMonitoringSwipeUpGesture() { - bubbleStackView = Mockito.spy(bubbleStackView) + bubbleStackView = spy(bubbleStackView) InstrumentationRegistry.getInstrumentation().runOnMainSync { // No way to add to window in the test environment right now so just pretend bubbleStackView.onDetachedFromWindow() diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt index af238d033aee..3499ee32e649 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt @@ -29,7 +29,8 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever /** Test for [UiEventSubject] */ @@ -130,10 +131,10 @@ class UiEventSubjectTest { } private fun createBubble(appUid: Int, packageName: String, instanceId: InstanceId): Bubble { - return mock(Bubble::class.java).apply { - whenever(getAppUid()).thenReturn(appUid) - whenever(getPackageName()).thenReturn(packageName) - whenever(getInstanceId()).thenReturn(instanceId) + return mock<Bubble>() { + on { getAppUid() } doReturn appUid + on { getPackageName() } doReturn packageName + on { getInstanceId() } doReturn instanceId } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index c022a298e972..7b5831376dc0 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -73,7 +73,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -127,7 +126,7 @@ class BubbleBarLayerViewTest { mainExecutor, bgExecutor, ) - bubbleController.asBubbles().setSysuiProxy(mock(SysuiProxy::class.java)) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) // Flush so that proxy gets set mainExecutor.flushAll() diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index a2231dd64112..1b7daa87064a 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -290,7 +290,7 @@ <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> <string name="fullscreen_text">Fullscreen</string> <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> - <string name="desktop_text">Desktop Mode</string> + <string name="desktop_text">Desktop View</string> <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> <string name="split_screen_text">Split Screen</string> <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> @@ -316,7 +316,7 @@ <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> - <string name="desktop_mode_app_header_chip_text">Open Menu</string> + <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> @@ -342,10 +342,10 @@ <!-- Accessibility text for the Maximize Menu's snap maximize/restore [CHAR LIMIT=NONE] --> <string name="desktop_mode_a11y_action_maximize_restore">Maximize or restore window size</string> - <!-- Accessibility action replacement for caption handle menu split screen button [CHAR LIMIT=NONE] --> - <string name="app_handle_menu_talkback_split_screen_mode_button_text">Enter split screen mode</string> - <!-- Accessibility action replacement for caption handle menu enter desktop mode button [CHAR LIMIT=NONE] --> - <string name="app_handle_menu_talkback_desktop_mode_button_text">Enter desktop windowing mode</string> + <!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] --> + <string name="app_handle_chip_accessibility_announce">Open Menu</string> + <!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string> <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 0b1f76f5ce0e..d280083ae7f5 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -17,4 +17,23 @@ <resources> <dimen name="floating_dismiss_icon_size">32dp</dimen> <dimen name="floating_dismiss_background_size">96dp</dimen> + + <!-- Bubble drag zone dimensions --> + <dimen name="drag_zone_dismiss_fold">140dp</dimen> + <dimen name="drag_zone_dismiss_tablet">200dp</dimen> + <dimen name="drag_zone_bubble_fold">140dp</dimen> + <dimen name="drag_zone_bubble_tablet">200dp</dimen> + <dimen name="drag_zone_full_screen_width">512dp</dimen> + <dimen name="drag_zone_full_screen_height">44dp</dimen> + <dimen name="drag_zone_desktop_window_width">880dp</dimen> + <dimen name="drag_zone_desktop_window_height">300dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen> + <dimen name="drag_zone_split_from_bubble_height">100dp</dimen> + <dimen name="drag_zone_split_from_bubble_width">60dp</dimen> + <dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index 35802c936361..909e9d2c4428 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -16,11 +16,15 @@ package com.android.wm.shell.shared.bubbles +import android.content.Context import android.graphics.Rect +import androidx.annotation.DimenRes +import com.android.wm.shell.shared.R import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode /** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */ class DragZoneFactory( + private val context: Context, private val deviceConfig: DeviceConfig, private val splitScreenModeChecker: SplitScreenModeChecker, private val desktopWindowModeChecker: DesktopWindowModeChecker, @@ -29,23 +33,65 @@ class DragZoneFactory( private val windowBounds: Rect get() = deviceConfig.windowBounds - // TODO b/393172431: move these to xml - private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200 - private val bubbleDragZoneTabletSize = 200 - private val bubbleDragZoneFoldableSize = 140 - private val fullScreenDragZoneWidth = 512 - private val fullScreenDragZoneHeight = 44 - private val desktopWindowDragZoneWidth = 880 - private val desktopWindowDragZoneHeight = 300 - private val desktopWindowFromExpandedViewDragZoneWidth = 200 - private val desktopWindowFromExpandedViewDragZoneHeight = 350 - private val splitFromBubbleDragZoneHeight = 100 - private val splitFromBubbleDragZoneWidth = 60 - private val hSplitFromExpandedViewDragZoneWidth = 60 - private val vSplitFromExpandedViewDragZoneWidth = 200 - private val vSplitFromExpandedViewDragZoneHeightTablet = 285 - private val vSplitFromExpandedViewDragZoneHeightFoldTall = 150 - private val vSplitFromExpandedViewDragZoneHeightFoldShort = 100 + private var dismissDragZoneSize = 0 + private var bubbleDragZoneTabletSize = 0 + private var bubbleDragZoneFoldableSize = 0 + private var fullScreenDragZoneWidth = 0 + private var fullScreenDragZoneHeight = 0 + private var desktopWindowDragZoneWidth = 0 + private var desktopWindowDragZoneHeight = 0 + private var desktopWindowFromExpandedViewDragZoneWidth = 0 + private var desktopWindowFromExpandedViewDragZoneHeight = 0 + private var splitFromBubbleDragZoneHeight = 0 + private var splitFromBubbleDragZoneWidth = 0 + private var hSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneHeightTablet = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0 + + init { + onConfigurationUpdated() + } + + /** Updates all dimensions after a configuration change. */ + fun onConfigurationUpdated() { + dismissDragZoneSize = + if (deviceConfig.isSmallTablet) { + context.resolveDimension(R.dimen.drag_zone_dismiss_fold) + } else { + context.resolveDimension(R.dimen.drag_zone_dismiss_tablet) + } + bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet) + bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold) + fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width) + fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height) + desktopWindowDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_width) + desktopWindowDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_height) + desktopWindowFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width) + desktopWindowFromExpandedViewDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height) + splitFromBubbleDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height) + splitFromBubbleDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width) + hSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneHeightTablet = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet) + vSplitFromExpandedViewDragZoneHeightFoldTall = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall) + vSplitFromExpandedViewDragZoneHeightFoldShort = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short) + } + + private fun Context.resolveDimension(@DimenRes dimension: Int) = + resources.getDimensionPixelSize(dimension) /** * Creates the list of drag zones for the dragged object. diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt index f234ff5c2c84..c545d3001cc7 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION +import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS import android.window.DesktopModeFlags import com.android.internal.R import com.android.window.flags.Flags @@ -59,13 +60,16 @@ class DesktopModeCompatPolicy(private val context: Context) { * The treatment is enabled when all the of the following is true: * * Any flags to forcibly consume caption insets are enabled. * * Top activity have configuration coupled with insets. - * * Task is not resizeable. + * * Task is not resizeable or [ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS] + * is enabled. */ fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean = Flags.excludeCaptionFromAppBounds() && isAnyForceConsumptionFlagsEnabled() && taskInfo.topActivityInfo?.let { - isInsetsCoupledWithConfiguration(it) && !taskInfo.isResizeable + isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled( + OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS + )) } ?: false /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index b2b99d648bf4..b6012378e4d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -914,12 +914,15 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + @DynamicOverride DesktopUserRepositories desktopUserRepositories, InteractionJankMonitor interactionJankMonitor) { return ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX.isTrue() ? new SpringDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor) + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor) : new DefaultDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor); + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 7491abd4248b..531304d6922a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1659,11 +1659,16 @@ class DesktopTasksController( private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { + + // If the wallpaper activity for this display already exists, let's reorder it to top. + val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId) + if (wallpaperActivityToken != null) { + wct.reorder(wallpaperActivityToken, /* onTop= */ true) + return + } + val intent = Intent(context, DesktopWallpaperActivity::class.java) - if ( - desktopWallpaperActivityTokenProvider.getToken(displayId) == null && - Flags.enablePerDisplayDesktopWallpaperActivity() - ) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index a5ba6612bb1a..c10752d36bf9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -90,6 +90,11 @@ class DesktopUserRepositories( return desktopRepoByUserId.getOrCreate(profileId) } + fun getUserIdForProfile(profileId: Int): Int { + if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId + else return profileId + } + /** Dumps [DesktopRepository] for each user. */ fun dump(pw: PrintWriter, prefix: String) { desktopRepoByUserId.forEach { key, value -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 2ac76f319d32..8194d3cab445 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -70,6 +70,7 @@ sealed class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val desktopUserRepositories: DesktopUserRepositories, protected val interactionJankMonitor: InteractionJankMonitor, protected val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { @@ -127,15 +128,18 @@ sealed class DragToDesktopTransitionHandler( pendingIntentCreatorBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED } - val taskUser = UserHandle.of(taskInfo.userId) + // If we are launching home for a profile of a user, just use the [userId] of that user + // instead of the [profileId] to create the context. + val userToLaunchWith = + UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId)) val pendingIntent = PendingIntent.getActivityAsUser( - context.createContextAsUser(taskUser, /* flags= */ 0), + context.createContextAsUser(userToLaunchWith, /* flags= */ 0), /* requestCode= */ 0, launchHomeIntent, FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT, options.toBundle(), - taskUser, + userToLaunchWith, ) val wct = WindowContainerTransaction() // The app that is being dragged into desktop mode might cause new transitions, make this @@ -881,6 +885,7 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() @@ -890,6 +895,7 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, transactionSupplier, ) { @@ -917,6 +923,7 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() @@ -926,6 +933,7 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, transactionSupplier, ) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java index b4cf8905d02e..88ac865c24b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java @@ -26,6 +26,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.hardware.HardwareBuffer; import android.util.TypedValue; import android.view.SurfaceControl; @@ -39,7 +40,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { private final Context mContext; private final int mAppIconSizePx; - private final Rect mAppBounds; private final int mOverlayHalfSize; private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; @@ -56,10 +56,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { final int overlaySize = getOverlaySize(appBounds, destinationBounds); mOverlayHalfSize = overlaySize >> 1; - // When the activity is in the secondary split, make sure the scaling center is not - // offset. - mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height()); - mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); mLeash = new SurfaceControl.Builder() @@ -85,12 +81,17 @@ public final class PipAppIconOverlay extends PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + final HardwareBuffer buffer = mBitmap.getHardwareBuffer(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); - tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setBuffer(mLeash, buffer); tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); + // Cleanup the bitmap and buffer after setting up the leash + mBitmap.recycle(); + mBitmap = null; + buffer.close(); } @Override @@ -108,16 +109,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } - - - @Override - public void detach(SurfaceControl.Transaction tx) { - super.detach(tx); - if (mBitmap != null && !mBitmap.isRecycled()) { - mBitmap.recycle(); - } - } - private void prepareAppIconOverlay(Drawable appIcon) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); @@ -139,6 +130,8 @@ public final class PipAppIconOverlay extends PipContentOverlay { mOverlayHalfSize + mAppIconSizePx / 2); appIcon.setBounds(appIconBounds); appIcon.draw(canvas); + Bitmap oldBitmap = mBitmap; mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + oldBitmap.recycle(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index bb9b479524e5..a57b4b948b42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -72,7 +72,6 @@ import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; import com.android.wm.shell.pip2.animation.PipExpandAnimator; import com.android.wm.shell.shared.TransitionUtil; -import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -422,7 +421,7 @@ public class PipTransition extends PipTransitionController implements final Rect destinationBounds = pipChange.getEndAbsBounds(); final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay(); if (swipePipToHomeOverlay != null) { - final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( + final int overlaySize = PipAppIconOverlay.getOverlaySize( mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); // It is possible we reparent the PIP activity to a new PIP task (in multi-activity // apps), so we should also reparent the overlay to the final PIP task. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 15ac03ccaf30..a799b7f2580e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2859,14 +2859,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED); mSplitTransitions.setDismissTransition(transition, dismissTop, EXIT_REASON_APP_FINISHED); - } else if (isOpening && !mPausingTasks.isEmpty()) { - // One of the splitting task is opening while animating the split pair in - // recents, which means to dismiss the split pair to this task. - int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN - ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; - prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED); - mSplitTransitions.setDismissTransition(transition, dismissTop, - EXIT_REASON_APP_FINISHED); } else if (!isSplitScreenVisible() && isOpening) { // If split is running in the background and the trigger task is appearing into // split, prepare to enter split screen. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index c92e67f1a0c0..a17bcb39f1a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -35,10 +35,8 @@ import android.view.SurfaceControl import android.view.View import android.view.WindowInsets.Type.systemBars import android.view.WindowManager -import android.widget.Button import android.widget.ImageButton import android.widget.ImageView -import android.widget.TextView import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup import androidx.annotation.StringRes @@ -540,17 +538,35 @@ class HandleMenu( return@setOnTouchListener true } - with(context.resources) { - // Update a11y read out to say "double tap to enter desktop windowing mode" + with(context) { + // Update a11y announcement out to say "double tap to enter Fullscreen" + ViewCompat.replaceAccessibilityAction( + fullscreenBtn, ACTION_CLICK, + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.fullscreen_text) + ), + null, + ) + + // Update a11y announcement out to say "double tap to enter Desktop View" ViewCompat.replaceAccessibilityAction( desktopBtn, ACTION_CLICK, - getString(R.string.app_handle_menu_talkback_desktop_mode_button_text), null + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.desktop_text) + ), + null, ) - // Update a11y read out to say "double tap to enter split screen mode" + // Update a11y announcement to say "double tap to enter Split Screen" ViewCompat.replaceAccessibilityAction( splitscreenBtn, ACTION_CLICK, - getString(R.string.app_handle_menu_talkback_split_screen_mode_button_text), null + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.split_screen_text) + ), + null, ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 4762bc21d79c..90c865e502fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -93,9 +93,6 @@ class AppHeaderViewHolder( private val lightColors = dynamicLightColorScheme(context) private val darkColors = dynamicDarkColorScheme(context) - private val headerButtonOpenMenuA11yText = context.resources - .getString(R.string.desktop_mode_app_header_chip_text) - /** * The corner radius to apply to the app chip, maximize and close button's background drawable. **/ @@ -231,35 +228,29 @@ class AppHeaderViewHolder( } } - val a11yActionOpenHeaderMenu = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, - headerButtonOpenMenuA11yText) - openMenuButton.accessibilityDelegate = object : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction(a11yActionOpenHeaderMenu) - } - } + // Update a11y announcement to say "double tap to open menu" + ViewCompat.replaceAccessibilityAction( + openMenuButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.app_handle_chip_accessibility_announce), + null + ) - with(context.resources) { - // Update a11y read out to say "double tap to maximize or restore window size" - ViewCompat.replaceAccessibilityAction( - maximizeWindowButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.maximize_button_talkback_action_maximize_restore_text), - null - ) + // Update a11y announcement to say "double tap to maximize or restore window size" + ViewCompat.replaceAccessibilityAction( + maximizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.maximize_button_talkback_action_maximize_restore_text), + null + ) - // Update a11y read out to say "double tap to minimize app window" - ViewCompat.replaceAccessibilityAction( - minimizeWindowButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.minimize_button_talkback_action_maximize_restore_text), - null - ) - } + // Update a11y announcement out to say "double tap to minimize app window" + ViewCompat.replaceAccessibilityAction( + minimizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.minimize_button_talkback_action_maximize_restore_text), + null + ) } override fun bindData(data: HeaderData) { @@ -275,7 +266,8 @@ class AppHeaderViewHolder( /** Sets the app's name in the header. */ fun setAppName(name: CharSequence) { appNameTextView.text = name - openMenuButton.contentDescription = name + openMenuButton.contentDescription = + context.getString(R.string.desktop_mode_app_header_chip_text, name) } /** Sets the app's icon in the header. */ diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index 02b2cec8dbdb..ae73dae99d6f 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -53,10 +53,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 98c5bc960c35..718bf322f6a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -171,7 +171,6 @@ import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock @@ -363,9 +362,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() shellInit.init() - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + val captor = argumentCaptor<RecentsTransitionStateListener>() verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value + recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener @@ -441,7 +440,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { val task1 = setUpFreeformTask() - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -461,7 +460,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() STABLE_BOUNDS.height(), displayController, ) - assertThat(argumentCaptor.value).isTrue() + assertThat(argumentCaptor.firstValue).isTrue() } @Test @@ -476,7 +475,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val task1 = setUpFreeformTask(bounds = stableBounds, active = true) - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -497,7 +496,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(displayController), anyOrNull(), ) - assertThat(argumentCaptor.value).isFalse() + assertThat(argumentCaptor.firstValue).isFalse() } @Test @@ -547,6 +546,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -581,7 +581,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) // Wallpaper is moved to front. - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 0, wallpaperToken) // Desk is activated. verify(desksOrganizer).activateDesk(wct, deskId) } @@ -783,6 +783,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskVisible(task1) @@ -825,7 +826,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -842,6 +844,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() wct.assertReorderAt(index = 2, task2) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertReorderAt(index = 0, wallpaperToken) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + @Test @DisableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, @@ -860,9 +880,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { - whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) - .thenReturn(Binder()) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = @@ -871,10 +891,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, - ) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertReorderAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -899,6 +927,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) .thenReturn(Binder()) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) @@ -991,6 +1020,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() @@ -1569,6 +1599,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) @@ -1736,7 +1767,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1751,12 +1782,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) @@ -1768,7 +1799,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test @@ -1802,6 +1833,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) @@ -1828,6 +1860,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() { val freeformTask = setUpFreeformTask() @@ -1840,7 +1873,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) val wct = getLatestEnterDesktopWct() - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 0, wallpaperToken) verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask) verify(desksOrganizer).activateDesk(wct, deskId = 0) verify(desktopModeEnterExitTransitionListener) @@ -1967,6 +2000,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() @@ -2224,26 +2258,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertThat(transitionHandlerArgCaptor.value) + assertThat(transitionHandlerArgCaptor.firstValue) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @@ -2718,9 +2752,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2759,9 +2793,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2775,9 +2809,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } } @Test @@ -2791,10 +2825,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is being minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2808,9 +2842,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // The only active task is already minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2825,9 +2859,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2845,10 +2879,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // task1 is the only visible task as task2 is minimized. controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) // Adds remove wallpaper operation - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -2987,6 +3021,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -3118,6 +3153,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -3152,7 +3188,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") @@ -3180,6 +3218,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -4635,7 +4674,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4643,9 +4682,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(task2.configuration.windowConfiguration.bounds), ) - assertThat(wctArgument.value.hierarchyOps).hasSize(1) + assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop - wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -4660,7 +4699,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4669,7 +4708,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(task2.configuration.windowConfiguration.bounds), ) // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wctArgument.value.hierarchyOps).isEmpty() + assertThat(wctArgument.firstValue.hierarchyOps).isEmpty() } @Test @@ -4677,7 +4716,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromFullscreenOpensInSplit() { setUpLandscapeDisplay() val task = setUpFullscreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4690,7 +4729,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4699,7 +4738,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromSplitOpensInSplit() { setUpLandscapeDisplay() val task = setUpSplitScreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4712,7 +4751,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4807,11 +4846,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpFullscreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4821,11 +4860,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpSplitScreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -5912,35 +5951,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockDragEvent, mockCallback as Consumer<Boolean>, ) - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() var expectedWindowingMode: Int if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { expectedWindowingMode = WINDOWING_MODE_FULLSCREEN // Fullscreen launches currently use default transitions - verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + verify(transitions).startTransition(any(), arg.capture(), anyOrNull()) } else { expectedWindowingMode = WINDOWING_MODE_FREEFORM if (tabTearingAnimationFlagEnabled) { verify(desktopMixedTransitionHandler) .startLaunchTransition( eq(TRANSIT_OPEN), - capture(arg), + arg.capture(), anyOrNull(), anyOrNull(), anyOrNull(), ) } else { // All other launches use a special handler. - verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture()) } } assertThat( - ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) .launchWindowingMode ) .isEqualTo(expectedWindowingMode) - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds) + assertThat( + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) + .launchBounds + ) .isEqualTo(expectedBounds) } @@ -6122,52 +6163,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out TransitionHandler>? = null, ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() if (handlerClass == null) { verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) } - return arg.value + return arg.lastValue } private fun getLatestToggleResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) - return arg.value + .startTransition(arg.capture(), eq(currentBounds)) + return arg.lastValue } private fun getLatestDesktopMixedTaskWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull()) - return arg.value + .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull()) + return arg.lastValue } private fun getLatestEnterDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - return arg.value + return arg.lastValue } private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - return arg.value + val arg = argumentCaptor<WindowContainerTransaction>() + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture()) + return arg.lastValue } private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - return arg.value + return arg.lastValue } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt index 83e48728c4f2..030bb1ace49d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt @@ -123,8 +123,26 @@ class DesktopUserRepositoriesTest : ShellTestCase() { assertThat(desktopRepository.userId).isEqualTo(PROFILE_ID_2) } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) + fun getUserForProfile_flagEnabled_returnsUserIdForProfile() { + userRepositories.onUserChanged(USER_ID_2, mock()) + val profiles: MutableList<UserInfo> = + mutableListOf( + UserInfo(USER_ID_2, "User profile", 0), + UserInfo(PROFILE_ID_1, "Work profile", 0), + ) + userRepositories.onUserProfilesChanged(profiles) + + val userIdForProfile = userRepositories.getUserIdForProfile(PROFILE_ID_1) + + assertThat(userIdForProfile).isEqualTo(USER_ID_2) + } + private companion object { const val USER_ID_1 = 7 + const val USER_ID_2 = 8 + const val PROFILE_ID_1 = 4 const val PROFILE_ID_2 = 5 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 25246d9984c3..1732875f1d57 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -70,6 +70,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var draggedTaskLeash: SurfaceControl @Mock private lateinit var homeTaskLeash: SurfaceControl + @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -84,6 +85,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, transactionSupplier, ) @@ -93,6 +95,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, transactionSupplier, ) @@ -484,17 +487,22 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() - val startTransition = startDrag( - springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null) + val startTransition = + startDrag( + springHandler, + task, + finishTransaction = playingFinishTransaction, + homeChange = null, + ) springHandler.onTaskResizeAnimationListener = mock() springHandler.mergeAnimation( transition = mock<IBinder>(), info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task, - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task, + ), startT = mergedStartTransaction, finishT = mergedFinishTransaction, mergeTarget = startTransition, @@ -723,7 +731,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo( type: Int, draggedTask: RunningTaskInfo, - homeChange: TransitionInfo.Change? = createHomeChange()) = + homeChange: TransitionInfo.Change? = createHomeChange(), + ) = TransitionInfo(type, /* flags= */ 0).apply { homeChange?.let { addChange(it) } addChange( // Dragged Task. @@ -741,11 +750,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } - private fun createHomeChange() = TransitionInfo.Change(mock(), homeTaskLeash).apply { - parent = null - taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - flags = flags or FLAG_IS_WALLPAPER - } + private fun createHomeChange() = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + flags = flags or FLAG_IS_WALLPAPER + } private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt index 7cd46af9402b..fd22a84dee5d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -16,8 +16,10 @@ package com.android.wm.shell.shared.bubbles +import android.content.Context import android.graphics.Insets import android.graphics.Rect +import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker @@ -34,6 +36,7 @@ private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit /** Unit tests for [DragZoneFactory]. */ class DragZoneFactoryTest { + private val context = getApplicationContext<Context>() private lateinit var dragZoneFactory: DragZoneFactory private val tabletPortrait = DeviceConfig( @@ -57,7 +60,12 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubbleBar_tablet() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT)) val expectedZones: List<DragZoneVerifier> = @@ -73,7 +81,12 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubble_tablet_portrait() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) val expectedZones: List<DragZoneVerifier> = @@ -92,7 +105,13 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubble_tablet_landscape() { - dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) val expectedZones: List<DragZoneVerifier> = @@ -111,7 +130,13 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubble_foldable_portrait() { - dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) val expectedZones: List<DragZoneVerifier> = @@ -129,7 +154,13 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubble_foldable_landscape() { - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) val expectedZones: List<DragZoneVerifier> = @@ -148,7 +179,12 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_tablet_portrait() { dragZoneFactory = - DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones( DraggedObject.ExpandedView(BubbleBarLocation.LEFT) @@ -169,9 +205,17 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_tablet_landscape() { - dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) val expectedZones: List<DragZoneVerifier> = listOf( verifyInstance<DragZone.Dismiss>(), @@ -188,9 +232,17 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_foldable_portrait() { - dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) val expectedZones: List<DragZoneVerifier> = listOf( verifyInstance<DragZone.Dismiss>(), @@ -206,9 +258,17 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_foldable_landscape() { - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) val expectedZones: List<DragZoneVerifier> = listOf( verifyInstance<DragZone.Dismiss>(), @@ -225,7 +285,13 @@ class DragZoneFactoryTest { @Test fun dragZonesForBubble_tablet_desktopModeDisabled() { isDesktopWindowModeSupported = false - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() @@ -234,9 +300,17 @@ class DragZoneFactoryTest { @Test fun dragZonesForExpandedView_tablet_desktopModeDisabled() { isDesktopWindowModeSupported = false - dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) val dragZones = - dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt index 55e9de5eff5f..ae1e4e0fbbc1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt @@ -181,6 +181,17 @@ class DesktopModeCompatPolicyTest : ShellTestCase() { ) } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + @EnableCompatChanges(ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS) + fun testShouldExcludeCaptionFromAppBounds_resizeable_overridden_true() { + assertTrue(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds( + setUpFreeformTask().apply { isResizeable = true }) + ) + } + fun setUpFreeformTask(): TaskInfo = createFreeformTask().apply { val componentName = diff --git a/media/java/android/media/audiofx/HapticGenerator.java b/media/java/android/media/audiofx/HapticGenerator.java index d2523ef43b9e..7f94ddea9b84 100644 --- a/media/java/android/media/audiofx/HapticGenerator.java +++ b/media/java/android/media/audiofx/HapticGenerator.java @@ -36,6 +36,20 @@ import java.util.UUID; * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio * effects. + * + * <pre>{@code + * AudioManager audioManager = context.getSystemService(AudioManager.class); + * player = MediaPlayer.create( + * context, + * audioUri, + * new AudioAttributes.Builder().setHapticChannelsMuted(false).build(), + * audioManager.generateAudioSessionId() + * ); + * if (HapticGenerator.isAvailable()) { + * HapticGenerator.create(player.getAudioSessionId()).setEnabled(true); + * } + * player.start(); + * }</pre> */ public class HapticGenerator extends AudioEffect implements AutoCloseable { diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_india.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_india.kcm new file mode 100644 index 000000000000..0059d0040be4 --- /dev/null +++ b/packages/InputDevices/res/raw/keyboard_layout_english_india.kcm @@ -0,0 +1,400 @@ +# Copyright 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# English (India) keyboard layout. +# + +type OVERLAY + +map key 86 PLUS + +### ROW 1 + +key GRAVE { + label: '`' + base: '`' + shift: '~' + ralt: '\u0300' + ralt+shift: '\u0303' +} + +key 1 { + label: '1' + base: '1' + shift: '!' +} + +key 2 { + label: '2' + base: '2' + shift: '@' +} + +key 3 { + label: '3' + base: '3' + shift: '#' +} + +key 4 { + label: '4' + base: '4' + shift: '$' + ralt, ctrl+shift: '\u20b9' +} + +key 5 { + label: '5' + base: '5' + shift: '%' +} + +key 6 { + label: '6' + base: '6' + shift: '^' + ralt+shift: '\u0302' +} + +key 7 { + label: '7' + base: '7' + shift: '&' +} + +key 8 { + label: '8' + base: '8' + shift: '*' +} + +key 9 { + label: '9' + base: '9' + shift: '(' + ralt+shift: '\u0306' +} + +key 0 { + label: '0' + base: '0' + shift: ')' +} + +key MINUS { + label: '-' + base: '-' + shift: '_' + ralt+shift: '\u0331' +} + +key EQUALS { + label: '=' + base: '=' + shift: '+' + ralt: '\u2013' + ralt+shift: '\u2014' +} + +### ROW 2 + +key Q { + label: 'Q' + base: 'q' + shift, capslock: 'Q' + capslock+shift: 'q' + ralt: '\u00e6' + ralt+shift, ralt+capslock: '\u00c6' + ralt+shift+capslock: '\u00e6' +} + +key W { + label: 'W' + base: 'w' + shift, capslock: 'W' + capslock+shift: 'w' +} + +key E { + label: 'E' + base: 'e' + shift, capslock: 'E' + capslock+shift: 'e' + ralt: '\u0113' + ralt+shift, ralt+capslock: '\u0112' + ralt+shift+capslock: '\u0113' +} + +key R { + label: 'R' + base: 'r' + shift, capslock: 'R' + capslock+shift: 'r' +} + +key T { + label: 'T' + base: 't' + shift, capslock: 'T' + capslock+shift: 't' + ralt: '\u1e6d' + ralt+shift, ralt+capslock: '\u1e6c' + ralt+shift+capslock: '\u1e6d' +} + +key Y { + label: 'Y' + base: 'y' + shift, capslock: 'Y' + capslock+shift: 'y' + ralt: '\u00f1' + ralt+shift, ralt+capslock: '\u00d1' + ralt+shift+capslock: '\u00f1' +} + +key U { + label: 'U' + base: 'u' + shift, capslock: 'U' + capslock+shift: 'u' + ralt: '\u016b' + ralt+shift, ralt+capslock: '\u016a' + ralt+shift+capslock: '\u016b' +} + +key I { + label: 'I' + base: 'i' + shift, capslock: 'I' + capslock+shift: 'i' + ralt: '\u012b' + ralt+shift, ralt+capslock: '\u012a' + ralt+shift+capslock: '\u012b' +} + +key O { + label: 'O' + base: 'o' + shift, capslock: 'O' + capslock+shift: 'o' + ralt: '\u014d' + ralt+shift, ralt+capslock: '\u014c' + ralt+shift+capslock: '\u014d' +} + +key P { + label: 'P' + base: 'p' + shift, capslock: 'P' + capslock+shift: 'p' +} + +key LEFT_BRACKET { + label: '[' + base: '[' + shift: '{' +} + +key RIGHT_BRACKET { + label: ']' + base: ']' + shift: '}' +} + +### ROW 3 + +key A { + label: 'A' + base: 'a' + shift, capslock: 'A' + capslock+shift: 'a' + ralt: '\u0101' + ralt+shift, ralt+capslock: '\u0100' + ralt+shift+capslock: '\u0101' +} + +key S { + label: 'S' + base: 's' + shift, capslock: 'S' + capslock+shift: 's' + ralt: '\u015b' + ralt+shift, ralt+capslock: '\u015a' + ralt+shift+capslock: '\u015b' +} + +key D { + label: 'D' + base: 'd' + shift, capslock: 'D' + capslock+shift: 'd' + ralt: '\u1e0d' + ralt+shift, ralt+capslock: '\u1e0c' + ralt+shift+capslock: '\u1e0d' +} + +key F { + label: 'F' + base: 'f' + shift, capslock: 'F' + capslock+shift: 'f' +} + +key G { + label: 'G' + base: 'g' + shift, capslock: 'G' + capslock+shift: 'g' + ralt: '\u1e45' + ralt+shift, ralt+capslock: '\u1e44' + ralt+shift+capslock: '\u1e45' +} + +key H { + label: 'H' + base: 'h' + shift, capslock: 'H' + capslock+shift: 'h' + ralt: '\u1e25' + ralt+shift, ralt+capslock: '\u1e24' + ralt+shift+capslock: '\u1e25' +} + +key J { + label: 'J' + base: 'j' + shift, capslock: 'J' + capslock+shift: 'j' +} + +key K { + label: 'K' + base: 'k' + shift, capslock: 'K' + capslock+shift: 'k' +} + +key L { + label: 'L' + base: 'l' + shift, capslock: 'L' + capslock+shift: 'l' +} + +key SEMICOLON { + label: ';' + base: ';' + shift: ':' +} + +key APOSTROPHE { + label: '\'' + base: '\'' + shift: '\u0022' + ralt: '\u030d' + ralt+shift: '\u030e' +} + +key BACKSLASH { + label: '\\' + base: '\\' + shift: '|' +} + +### ROW 4 + +key PLUS { + label: '\\' + base: '\\' + shift: '|' +} + +key Z { + label: 'Z' + base: 'z' + shift, capslock: 'Z' + capslock+shift: 'z' +} + +key X { + label: 'X' + base: 'x' + shift, capslock: 'X' + capslock+shift: 'x' + ralt: '\u1e63' + ralt+shift, ralt+capslock: '\u1e62' + ralt+shift+capslock: '\u1e63' +} + +key C { + label: 'C' + base: 'c' + shift, capslock: 'C' + capslock+shift: 'c' +} + +key V { + label: 'V' + base: 'v' + shift, capslock: 'V' + capslock+shift: 'v' +} + +key B { + label: 'B' + base: 'b' + shift, capslock: 'B' + capslock+shift: 'b' +} + +key N { + label: 'N' + base: 'n' + shift, capslock: 'N' + capslock+shift: 'n' + ralt: '\u1e47' + ralt+shift, ralt+capslock: '\u1e46' + ralt+shift+capslock: '\u1e47' +} + +key M { + label: 'M' + base: 'm' + shift, capslock: 'M' + capslock+shift: 'm' + ralt: '\u1e41' + ralt+shift, ralt+capslock: '\u1e40' + ralt+shift+capslock: '\u1e41' +} + +key COMMA { + label: ',' + base: ',' + shift: '<' + ralt+shift: '\u030C' +} + +key PERIOD { + label: '.' + base: '.' + shift: '>' + ralt: '\u0323' +} + +key SLASH { + label: '/' + base: '/' + shift: '?' +} diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml index bd7cdc481524..8a397a5e9d18 100644 --- a/packages/InputDevices/res/values/strings.xml +++ b/packages/InputDevices/res/values/strings.xml @@ -167,4 +167,7 @@ <!-- Romanian keyboard layout label. [CHAR LIMIT=35] --> <string name="keyboard_layout_romanian">Romanian</string> + + <!-- English (India) keyboard layout label. [CHAR LIMIT=35] --> + <string name="keyboard_layout_english_india">English (India)</string> </resources> diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml index 9ce9a87a1f9f..fa0ed13fb32c 100644 --- a/packages/InputDevices/res/xml/keyboard_layouts.xml +++ b/packages/InputDevices/res/xml/keyboard_layouts.xml @@ -367,4 +367,11 @@ android:keyboardLayout="@raw/keyboard_layout_romanian" android:keyboardLocale="ro-Latn-RO" android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_english_india" + android:label="@string/keyboard_layout_english_india" + android:keyboardLayout="@raw/keyboard_layout_english_india" + android:keyboardLocale="en-Latn-IN" + android:keyboardLayoutType="qwerty" /> </keyboard-layouts> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index bf86911ee683..572444edea29 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -30,11 +30,13 @@ import android.util.Log; import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -385,7 +387,7 @@ public class CsipDeviceManager { preferredMainDevice.refresh(); hasChanged = true; } - syncAudioSharingSourceIfNeeded(preferredMainDevice); + syncAudioSharingStatusIfNeeded(preferredMainDevice); } if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " @@ -399,13 +401,16 @@ public class CsipDeviceManager { return userManager != null && userManager.isManagedProfile(); } - private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) { + private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) { boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext); - if (isAudioSharingEnabled) { + if (isAudioSharingEnabled && mainDevice != null) { if (isWorkProfile()) { - log("addMemberDevicesIntoMainDevice: skip sync source for work profile"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile"); return; } + Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); + deviceSet.add(mainDevice); + deviceSet.addAll(mainDevice.getMemberDevice()); boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager) && BluetoothUtils.hasConnectedBroadcastSource( mainDevice, mBtManager); @@ -419,9 +424,6 @@ public class CsipDeviceManager { if (metadata != null && assistant != null) { log("addMemberDevicesIntoMainDevice: sync audio sharing source after " + "combining the top level devices."); - Set<CachedBluetoothDevice> deviceSet = new HashSet<>(); - deviceSet.add(mainDevice); - deviceSet.addAll(mainDevice.getMemberDevice()); Set<BluetoothDevice> sinksToSync = deviceSet.stream() .map(CachedBluetoothDevice::getDevice) .filter(device -> @@ -435,8 +437,24 @@ public class CsipDeviceManager { } } } + if (Flags.enableTemporaryBondDevicesUi()) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing " + + "sinks after combining the top level devices."); + Set<BluetoothDevice> sinksToSync = deviceSet.stream() + .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect( + Collectors.toSet()); + if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) { + for (BluetoothDevice device : sinksToSync) { + if (!BluetoothUtils.isTemporaryBondDevice(device)) { + log("addMemberDevicesIntoMainDevice: sync temp bond metadata for " + + device.getAnonymizedAddress()); + BluetoothUtils.setTemporaryBondMetadata(device); + } + } + } + } } else { - log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled"); + log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled"); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt index 10156c404ebf..bac564c7d0f4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.media.MediaMetadata import android.media.session.MediaController +import android.media.session.MediaController.PlaybackInfo import android.media.session.MediaSession import android.media.session.MediaSessionManager import android.media.session.PlaybackState @@ -98,16 +99,22 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { } /** Set volume `level` to remote media `token` */ - fun setVolume(token: MediaSession.Token, level: Int) { + fun setVolume(sessionId: SessionId, volumeLevel: Int) { + when (sessionId) { + is SessionId.Media -> setMediaSessionVolume(sessionId.token, volumeLevel) + } + } + + private fun setMediaSessionVolume(token: MediaSession.Token, volumeLevel: Int) { val record = mRecords[token] if (record == null) { Log.w(TAG, "setVolume: No record found for token $token") return } if (D.BUG) { - Log.d(TAG, "Setting level to $level") + Log.d(TAG, "Setting level to $volumeLevel") } - record.controller.setVolumeTo(level, 0) + record.controller.setVolumeTo(volumeLevel, 0) } private fun onRemoteVolumeChangedH(sessionToken: MediaSession.Token, flags: Int) { @@ -122,7 +129,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { ) } val token = controller.sessionToken - mCallbacks.onRemoteVolumeChanged(token, flags) + mCallbacks.onRemoteVolumeChanged(SessionId.from(token), flags) } private fun onUpdateRemoteSessionListH(sessionToken: MediaSession.Token?) { @@ -158,7 +165,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { controller.registerCallback(record, mHandler) } val record = mRecords[token] - val remote = isRemote(playbackInfo) + val remote = playbackInfo.isRemote() if (remote) { updateRemoteH(token, record!!.name, playbackInfo) record.sentRemote = true @@ -172,7 +179,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { Log.d(TAG, "Removing " + record.name + " sentRemote=" + record.sentRemote) } if (record.sentRemote) { - mCallbacks.onRemoteRemoved(token) + mCallbacks.onRemoteRemoved(SessionId.from(token)) record.sentRemote = false } } @@ -213,8 +220,8 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { private fun updateRemoteH( token: MediaSession.Token, name: String?, - pi: MediaController.PlaybackInfo, - ) = mCallbacks.onRemoteUpdate(token, name, pi) + playbackInfo: PlaybackInfo, + ) = mCallbacks.onRemoteUpdate(SessionId.from(token), name, VolumeInfo.from(playbackInfo)) private inner class MediaControllerRecord(val controller: MediaController) : MediaController.Callback() { @@ -225,7 +232,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { return method + " " + controller.packageName + " " } - override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { + override fun onAudioInfoChanged(info: PlaybackInfo) { if (D.BUG) { Log.d( TAG, @@ -235,9 +242,9 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { sentRemote), ) } - val remote = isRemote(info) + val remote = info.isRemote() if (!remote && sentRemote) { - mCallbacks.onRemoteRemoved(controller.sessionToken) + mCallbacks.onRemoteRemoved(SessionId.from(controller.sessionToken)) sentRemote = false } else if (remote) { updateRemoteH(controller.sessionToken, name, info) @@ -301,20 +308,36 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { } } + /** Opaque id for ongoing sessions that support volume adjustment. */ + sealed interface SessionId { + + companion object { + fun from(token: MediaSession.Token) = Media(token) + } + + data class Media(val token: MediaSession.Token) : SessionId + } + + /** Holds session volume information. */ + data class VolumeInfo(val currentVolume: Int, val maxVolume: Int) { + + companion object { + + fun from(playbackInfo: PlaybackInfo) = + VolumeInfo(playbackInfo.currentVolume, playbackInfo.maxVolume) + } + } + /** Callback for remote media sessions */ interface Callbacks { /** Invoked when remote media session is updated */ - fun onRemoteUpdate( - token: MediaSession.Token?, - name: String?, - pi: MediaController.PlaybackInfo?, - ) + fun onRemoteUpdate(token: SessionId?, name: String?, volumeInfo: VolumeInfo?) /** Invoked when remote media session is removed */ - fun onRemoteRemoved(token: MediaSession.Token?) + fun onRemoteRemoved(token: SessionId?) /** Invoked when remote volume is changed */ - fun onRemoteVolumeChanged(token: MediaSession.Token?, flags: Int) + fun onRemoteVolumeChanged(token: SessionId?, flags: Int) } companion object { @@ -325,12 +348,11 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) { const val UPDATE_REMOTE_SESSION_LIST: Int = 3 private const val USE_SERVICE_LABEL = false - - private fun isRemote(pi: MediaController.PlaybackInfo?): Boolean = - pi != null && pi.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE } } +private fun PlaybackInfo?.isRemote() = this?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE + private fun MediaController.dump(n: Int, writer: PrintWriter) { writer.println(" Controller $n: $packageName") diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index fd14d1ff6786..2eccaa626f3b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -40,6 +40,8 @@ import android.content.Context; import android.os.Looper; import android.os.Parcel; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.flags.Flags; @@ -74,6 +76,9 @@ public class CsipDeviceManagerTest { private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String TEMP_BOND_METADATA = + "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; private final static int GROUP1 = 1; private final BluetoothClass DEVICE_CLASS_1 = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); @@ -337,6 +342,7 @@ public class CsipDeviceManagerTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list @@ -346,7 +352,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -359,6 +364,7 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() { // Condition: The preferredDevice is main and there is another main device in top list @@ -369,7 +375,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -377,6 +382,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); when(mUserManager.isManagedProfile()).thenReturn(true); @@ -387,10 +394,13 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() { // Condition: The preferredDevice is main and there is another main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice1; @@ -399,7 +409,6 @@ public class CsipDeviceManagerTest { mCachedDevices.add(preferredDevice); mCachedDevices.add(mCachedDevice2); mCachedDevices.add(mCachedDevice3); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -407,6 +416,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -415,6 +426,8 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false); + verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test @@ -436,13 +449,13 @@ public class CsipDeviceManagerTest { } @Test + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(false); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) @@ -457,16 +470,20 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant, never()).addSource(any(BluetoothDevice.class), any(BluetoothLeBroadcastMetadata.class), anyBoolean()); + verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test - public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() { + @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI}) + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() { // Condition: The preferredDevice is member and there are two main device in top list // Expected Result: return true and there is the preferredDevice in top list CachedBluetoothDevice preferredDevice = mCachedDevice2; BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); mCachedDevice3.setGroupId(GROUP1); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class); when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata); @@ -474,6 +491,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state)); + when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -488,6 +507,10 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false); verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false); + verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); } @Test diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 246aa7158cab..85617bad1a91 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -809,7 +809,9 @@ public class SettingsBackupTest { Settings.Secure.DND_CONFIGS_MIGRATED, Settings.Secure.NAVIGATION_MODE_RESTORE, Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, - Settings.Secure.V_TO_U_RESTORE_DENYLIST); + Settings.Secure.V_TO_U_RESTORE_DENYLIST, + Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI, + Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY); @Test public void systemSettingsBackedUpOrDenied() { diff --git a/packages/Shell/src/com/android/shell/BugreportPrefs.java b/packages/Shell/src/com/android/shell/BugreportPrefs.java index 93690d48cd04..b0fd925daec3 100644 --- a/packages/Shell/src/com/android/shell/BugreportPrefs.java +++ b/packages/Shell/src/com/android/shell/BugreportPrefs.java @@ -23,25 +23,24 @@ import android.content.SharedPreferences; * Preferences related to bug reports. */ final class BugreportPrefs { - static final String PREFS_BUGREPORT = "bugreports"; - - private static final String KEY_WARNING_STATE = "warning-state"; - - static final int STATE_UNKNOWN = 0; - // Shows the warning dialog. - static final int STATE_SHOW = 1; - // Skips the warning dialog. - static final int STATE_HIDE = 2; static int getWarningState(Context context, int def) { - final SharedPreferences prefs = context.getSharedPreferences( - PREFS_BUGREPORT, Context.MODE_PRIVATE); - return prefs.getInt(KEY_WARNING_STATE, def); + String prefsBugreport = context.getResources().getString( + com.android.internal.R.string.prefs_bugreport); + String keyWarningState = context.getResources().getString( + com.android.internal.R.string.key_warning_state); + final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport, + Context.MODE_PRIVATE); + return prefs.getInt(keyWarningState, def); } static void setWarningState(Context context, int value) { - final SharedPreferences prefs = context.getSharedPreferences( - PREFS_BUGREPORT, Context.MODE_PRIVATE); - prefs.edit().putInt(KEY_WARNING_STATE, value).apply(); + String prefsBugreport = context.getResources().getString( + com.android.internal.R.string.prefs_bugreport); + String keyWarningState = context.getResources().getString( + com.android.internal.R.string.key_warning_state); + final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport, + Context.MODE_PRIVATE); + prefs.edit().putInt(keyWarningState, value).apply(); } } diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 61f49db07abc..fb0678fedb56 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -21,8 +21,6 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.flags.Flags.handleBugreportsForWear; @@ -1347,7 +1345,11 @@ public class BugreportProgressService extends Service { } private boolean hasUserDecidedNotToGetWarningMessage() { - return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE; + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + return getWarningState(mContext, bugreportStateUnknown) == bugreportStateHide; } private void maybeShowWarningMessageAndCloseNotification(int id) { diff --git a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java index a44e23603f52..0e835f91aca6 100644 --- a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java +++ b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java @@ -16,9 +16,6 @@ package com.android.shell; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.BugreportPrefs.setWarningState; import static com.android.shell.BugreportProgressService.sendShareIntent; @@ -69,12 +66,19 @@ public class BugreportWarningActivity extends AlertActivity mConfirmRepeat = (CheckBox) ap.mView.findViewById(android.R.id.checkbox); - final int state = getWarningState(this, STATE_UNKNOWN); + int bugreportStateUnknown = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + int bugreportStateShow = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); + + final int state = getWarningState(this, bugreportStateUnknown); final boolean checked; if (Build.IS_USER) { - checked = state == STATE_HIDE; // Only checks if specifically set to. + checked = state == bugreportStateHide; // Only checks if specifically set to. } else { - checked = state != STATE_SHOW; // Checks by default. + checked = state != bugreportStateShow; // Checks by default. } mConfirmRepeat.setChecked(checked); @@ -83,9 +87,14 @@ public class BugreportWarningActivity extends AlertActivity @Override public void onClick(DialogInterface dialog, int which) { + int bugreportStateHide = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + int bugreportStateShow = getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); if (which == AlertDialog.BUTTON_POSITIVE) { // Remember confirm state, and launch target - setWarningState(this, mConfirmRepeat.isChecked() ? STATE_HIDE : STATE_SHOW); + setWarningState(this, mConfirmRepeat.isChecked() ? bugreportStateHide + : bugreportStateShow); if (mSendIntent != null) { sendShareIntent(this, mSendIntent); } diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 7bda2ea790b0..2d6abe6cdc93 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -19,10 +19,6 @@ package com.android.shell; import static android.test.MoreAsserts.assertContainsRegex; import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; -import static com.android.shell.BugreportPrefs.PREFS_BUGREPORT; -import static com.android.shell.BugreportPrefs.STATE_HIDE; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; import static com.android.shell.BugreportPrefs.getWarningState; import static com.android.shell.BugreportPrefs.setWarningState; import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_REQUESTED; @@ -201,8 +197,9 @@ public class BugreportReceiverTest { return null; }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(), any(), anyBoolean(), anyBoolean()); - - setWarningState(mContext, STATE_HIDE); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); + setWarningState(mContext, bugreportStateHide); mUiBot.turnScreenOn(); } @@ -469,22 +466,31 @@ public class BugreportReceiverTest { @Test public void testBugreportFinished_withWarningUnknownState() throws Exception { - bugreportFinishedWithWarningTest(STATE_UNKNOWN); + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + bugreportFinishedWithWarningTest(bugreportStateUnknown); } @Test public void testBugreportFinished_withWarningShowAgain() throws Exception { - bugreportFinishedWithWarningTest(STATE_SHOW); + int bugreportStateShow = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_show); + bugreportFinishedWithWarningTest(bugreportStateShow); } private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception { + int bugreportStateUnknown = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_unknown); + int bugreportStateHide = mContext.getResources().getInteger( + com.android.internal.R.integer.bugreport_state_hide); if (propertyState == null) { // Clear properties - mContext.getSharedPreferences(PREFS_BUGREPORT, Context.MODE_PRIVATE) - .edit().clear().commit(); + mContext.getSharedPreferences( + mContext.getResources().getString(com.android.internal.R.string.prefs_bugreport) + , Context.MODE_PRIVATE).edit().clear().commit(); // Confidence check... - assertEquals("Did not reset properties", STATE_UNKNOWN, - getWarningState(mContext, STATE_UNKNOWN)); + assertEquals("Did not reset properties", bugreportStateUnknown, + getWarningState(mContext, bugreportStateUnknown)); } else { setWarningState(mContext, propertyState); } @@ -501,7 +507,8 @@ public class BugreportReceiverTest { // TODO: get ok and dontShowAgain from the dialog reference above UiObject dontShowAgain = mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat)); - final boolean firstTime = propertyState == null || propertyState == STATE_UNKNOWN; + final boolean firstTime = + propertyState == null || propertyState == bugreportStateUnknown; if (firstTime) { if (Build.IS_USER) { assertFalse("Checkbox should NOT be checked by default on user builds", @@ -524,8 +531,8 @@ public class BugreportReceiverTest { assertActionSendMultiple(extras); // Make sure it's hidden now. - int newState = getWarningState(mContext, STATE_UNKNOWN); - assertEquals("Didn't change state", STATE_HIDE, newState); + int newState = getWarningState(mContext, bugreportStateUnknown); + assertEquals("Didn't change state", bugreportStateHide, newState); } @Test diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 2ca70558f18b..0b17a3f71bda 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -200,8 +200,9 @@ fun CommunalContainer( scene( CommunalScenes.Blank, userActions = - if (viewModel.v2FlagEnabled()) emptyMap() - else mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal), + if (viewModel.swipeToHubEnabled()) + mapOf(Swipe.Start(fromSource = Edge.End) to CommunalScenes.Communal) + else emptyMap(), ) { // This scene shows nothing only allowing for transitions to the communal scene. Box(modifier = Modifier.fillMaxSize()) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 9c57efc24a22..418a7a52a97e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1705,15 +1705,38 @@ private fun Umo( contentScope: ContentScope?, modifier: Modifier = Modifier, ) { - if (SceneContainerFlag.isEnabled && contentScope != null) { - contentScope.MediaCarousel( - modifier = modifier.fillMaxSize(), - isVisible = true, - mediaHost = viewModel.mediaHost, - carouselController = viewModel.mediaCarouselController, - ) - } else { - UmoLegacy(viewModel, modifier) + val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next) + val showPreviousActionLabel = + stringResource(R.string.accessibility_action_label_umo_show_previous) + + Box( + modifier = + modifier.thenIf(!viewModel.isEditMode) { + Modifier.semantics { + customActions = + listOf( + CustomAccessibilityAction(showNextActionLabel) { + viewModel.onShowNextMedia() + true + }, + CustomAccessibilityAction(showPreviousActionLabel) { + viewModel.onShowPreviousMedia() + true + }, + ) + } + } + ) { + if (SceneContainerFlag.isEnabled && contentScope != null) { + contentScope.MediaCarousel( + modifier = modifier.fillMaxSize(), + isVisible = true, + mediaHost = viewModel.mediaHost, + carouselController = viewModel.mediaCarouselController, + ) + } else { + UmoLegacy(viewModel, modifier) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt index 62aa31b49870..73a24257580c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.unit.times import androidx.window.layout.WindowMetricsCalculator import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_HEIGHT import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_WIDTH -import com.android.systemui.communal.util.WindowSizeUtils.MEDIUM_WIDTH /** * Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain @@ -267,9 +266,8 @@ fun calculateWindowSize(): DpSize { } private fun calculateNumCellsWidth(width: Dp) = - // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes when { - width >= MEDIUM_WIDTH -> 3 + width >= 900.dp -> 3 width >= COMPACT_WIDTH -> 2 else -> 1 } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 2e5b5b56c982..aad1276d76e5 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -113,8 +113,8 @@ class DefaultClockProvider( companion object { // 750ms @ 120hz -> 90 frames of animation - // In practice, 45 looks good enough - const val NUM_CLOCK_FONT_ANIMATION_STEPS = 45 + // In practice, 30 looks good enough and limits our memory usage + const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 val FLEX_TYPEFACE by lazy { // TODO(b/364680873): Move constant to config_clockFontFamily when shipping diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index fe665e658feb..24b9e847d621 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -84,6 +84,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor import com.google.common.truth.Truth import junit.framework.Assert import kotlinx.coroutines.flow.MutableStateFlow @@ -280,9 +281,9 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { kosmos.keyguardDismissTransitionInteractor, { primaryBouncerInteractor }, executor, - ) { - deviceEntryInteractor - } + { deviceEntryInteractor }, + { kosmos.windowRootViewBlurInteractor }, + ) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 18cc8bf5f0d3..522650bcde3c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -23,24 +23,19 @@ import android.content.pm.PackageManager import android.content.pm.UserInfo import android.provider.Settings import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityManager import android.view.accessibility.accessibilityManager import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.uiEventLogger +import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.model.CommunalSmartspaceTimer -import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository -import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository -import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor @@ -49,12 +44,15 @@ import com.android.systemui.communal.shared.log.CommunalMetricsLogger import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.mediaCarouselController import com.android.systemui.media.controls.ui.view.MediaHost @@ -62,73 +60,45 @@ import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.spy import org.mockito.kotlin.whenever +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { - @Mock private lateinit var mediaHost: MediaHost - @Mock private lateinit var uiEventLogger: UiEventLogger - @Mock private lateinit var packageManager: PackageManager - @Mock private lateinit var metricsLogger: CommunalMetricsLogger - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private lateinit var tutorialRepository: FakeCommunalTutorialRepository - private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository - private lateinit var mediaRepository: FakeCommunalMediaRepository - private lateinit var communalSceneInteractor: CommunalSceneInteractor - private lateinit var communalInteractor: CommunalInteractor - private lateinit var accessibilityManager: AccessibilityManager + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testableResources = context.orCreateTestableResources - private lateinit var underTest: CommunalEditModeViewModel + private val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() } - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - tutorialRepository = kosmos.fakeCommunalTutorialRepository - widgetRepository = kosmos.fakeCommunalWidgetRepository - smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository - mediaRepository = kosmos.fakeCommunalMediaRepository - communalSceneInteractor = kosmos.communalSceneInteractor - communalInteractor = spy(kosmos.communalInteractor) - kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) - kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) - kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) - accessibilityManager = kosmos.accessibilityManager + private val Kosmos.metricsLogger by Kosmos.Fixture { mock<CommunalMetricsLogger>() } - underTest = + private val Kosmos.underTest by + Kosmos.Fixture { CommunalEditModeViewModel( communalSceneInteractor, communalInteractor, - kosmos.communalSettingsInteractor, - kosmos.keyguardTransitionInteractor, - mediaHost, + communalSettingsInteractor, + keyguardTransitionInteractor, + mock<MediaHost>(), uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), - kosmos.testDispatcher, + testDispatcher, metricsLogger, context, accessibilityManager, @@ -136,19 +106,28 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { WIDGET_PICKER_PACKAGE_NAME, kosmos.mediaCarouselController, ) + } + + @Before + fun setUp() { + kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) } @Test fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() = - testScope.runTest { - tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + kosmos.runTest { + fakeCommunalTutorialRepository.setTutorialSettingState( + Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED + ) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, rank = 30) - widgetRepository.addWidget(appWidgetId = 1, rank = 20) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 0, rank = 30) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, rank = 20) // Smartspace available. - smartspaceRepository.setTimers( + fakeCommunalSmartspaceRepository.setTimers( listOf( CommunalSmartspaceTimer( smartspaceTargetId = "target", @@ -159,7 +138,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { ) // Media playing. - mediaRepository.mediaActive() + fakeCommunalMediaRepository.mediaActive() val communalContent by collectLastValue(underTest.communalContent) @@ -173,7 +152,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun selectedKey_onReorderWidgets_isSet() = - testScope.runTest { + kosmos.runTest { val selectedKey by collectLastValue(underTest.selectedKey) underTest.setSelectedKey(null) @@ -186,7 +165,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun isCommunalContentVisible_isTrue_whenEditModeShowing() = - testScope.runTest { + kosmos.runTest { val isCommunalContentVisible by collectLastValue(underTest.isCommunalContentVisible) communalSceneInteractor.setEditModeState(EditModeState.SHOWING) assertThat(isCommunalContentVisible).isEqualTo(true) @@ -194,7 +173,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun isCommunalContentVisible_isFalse_whenEditModeNotShowing() = - testScope.runTest { + kosmos.runTest { val isCommunalContentVisible by collectLastValue(underTest.isCommunalContentVisible) communalSceneInteractor.setEditModeState(null) assertThat(isCommunalContentVisible).isEqualTo(false) @@ -202,12 +181,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun deleteWidget() = - testScope.runTest { - tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + kosmos.runTest { + fakeCommunalTutorialRepository.setTutorialSettingState( + Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED + ) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, rank = 30) - widgetRepository.addWidget(appWidgetId = 1, rank = 20) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 0, rank = 30) + fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, rank = 20) val communalContent by collectLastValue(underTest.communalContent) @@ -233,26 +214,38 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test - fun reorderWidget_uiEventLogging_start() { - underTest.onReorderWidgetStart(CommunalContentModel.KEY.widget(123)) - verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) - } + fun reorderWidget_uiEventLogging_start() = + kosmos.runTest { + underTest.onReorderWidgetStart(CommunalContentModel.KEY.widget(123)) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START.id) + } @Test - fun reorderWidget_uiEventLogging_end() { - underTest.onReorderWidgetEnd() - verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) - } + fun reorderWidget_uiEventLogging_end() = + kosmos.runTest { + underTest.onReorderWidgetEnd() + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH.id) + } @Test - fun reorderWidget_uiEventLogging_cancel() { - underTest.onReorderWidgetCancel() - verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) - } + fun reorderWidget_uiEventLogging_cancel() = + kosmos.runTest { + underTest.onReorderWidgetCancel() + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL.id) + } @Test fun onOpenWidgetPicker_launchesWidgetPickerActivity() { - testScope.runTest { + kosmos.runTest { var activityStarted = false val success = underTest.onOpenWidgetPicker(testableResources.resources) { _ -> @@ -266,7 +259,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { - testScope.runTest { + kosmos.runTest { val success = underTest.onOpenWidgetPicker(testableResources.resources) { _ -> run { throw ActivityNotFoundException() } @@ -278,7 +271,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun showDisclaimer_trueAfterEditModeShowing() = - testScope.runTest { + kosmos.runTest { val showDisclaimer by collectLastValue(underTest.showDisclaimer) assertThat(showDisclaimer).isFalse() @@ -288,9 +281,9 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun showDisclaimer_falseWhenDismissed() = - testScope.runTest { + kosmos.runTest { underTest.setEditModeState(EditModeState.SHOWING) - kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) val showDisclaimer by collectLastValue(underTest.showDisclaimer) @@ -301,63 +294,67 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun showDisclaimer_trueWhenTimeout() = - testScope.runTest { + kosmos.runTest { underTest.setEditModeState(EditModeState.SHOWING) - kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) val showDisclaimer by collectLastValue(underTest.showDisclaimer) assertThat(showDisclaimer).isTrue() underTest.onDisclaimerDismissed() assertThat(showDisclaimer).isFalse() - advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + testScope.advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS + 1.milliseconds) assertThat(showDisclaimer).isTrue() } @Test - fun scrollPosition_persistedOnEditCleanup() { - val index = 2 - val offset = 30 - underTest.onScrollPositionUpdated(index, offset) - underTest.cleanupEditModeState() - - verify(communalInteractor).setScrollPosition(eq(index), eq(offset)) - } + fun scrollPosition_persistedOnEditCleanup() = + kosmos.runTest { + val index = 2 + val offset = 30 + underTest.onScrollPositionUpdated(index, offset) + underTest.cleanupEditModeState() + + assertThat(communalInteractor.firstVisibleItemIndex).isEqualTo(index) + assertThat(communalInteractor.firstVisibleItemOffset).isEqualTo(offset) + } @Test - fun onNewWidgetAdded_accessibilityDisabled_doNothing() { - whenever(accessibilityManager.isEnabled).thenReturn(false) + fun onNewWidgetAdded_accessibilityDisabled_doNothing() = + kosmos.runTest { + whenever(accessibilityManager.isEnabled).thenReturn(false) - val provider = - mock<AppWidgetProviderInfo> { - on { loadLabel(packageManager) }.thenReturn("Test Clock") - } - underTest.onNewWidgetAdded(provider) + val provider = + mock<AppWidgetProviderInfo> { + on { loadLabel(packageManager) }.thenReturn("Test Clock") + } + underTest.onNewWidgetAdded(provider) - verify(accessibilityManager, never()).sendAccessibilityEvent(any()) - } + verify(accessibilityManager, never()).sendAccessibilityEvent(any()) + } @Test - fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() { - whenever(accessibilityManager.isEnabled).thenReturn(true) + fun onNewWidgetAdded_accessibilityEnabled_sendAccessibilityAnnouncement() = + kosmos.runTest { + whenever(accessibilityManager.isEnabled).thenReturn(true) - val provider = - mock<AppWidgetProviderInfo> { - on { loadLabel(packageManager) }.thenReturn("Test Clock") - } - underTest.onNewWidgetAdded(provider) + val provider = + mock<AppWidgetProviderInfo> { + on { loadLabel(packageManager) }.thenReturn("Test Clock") + } + underTest.onNewWidgetAdded(provider) - val captor = argumentCaptor<AccessibilityEvent>() - verify(accessibilityManager).sendAccessibilityEvent(captor.capture()) + val captor = argumentCaptor<AccessibilityEvent>() + verify(accessibilityManager).sendAccessibilityEvent(captor.capture()) - val event = captor.firstValue - assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT) - assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen") - } + val event = captor.firstValue + assertThat(event.eventType).isEqualTo(AccessibilityEvent.TYPE_ANNOUNCEMENT) + assertThat(event.contentDescription).isEqualTo("Test Clock widget added to lock screen") + } @Test fun onResizeWidget_logsMetrics() = - testScope.runTest { + kosmos.runTest { val appWidgetId = 123 val spanY = 2 val widgetIdToRankMap = mapOf(appWidgetId to 1) @@ -372,7 +369,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { rank = rank, ) - verify(communalInteractor).resizeWidget(appWidgetId, spanY, widgetIdToRankMap) verify(metricsLogger) .logResizeWidget( componentName = componentName.flattenToString(), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 85155157eda2..799054a92bee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -78,6 +78,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.mediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -120,6 +121,7 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -161,6 +163,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) whenever(mediaHost.visible).thenReturn(true) + whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler) + .thenReturn(mediaCarouselScrollHandler) kosmos.powerInteractor.setAwakeForTest() @@ -187,6 +191,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { metricsLogger, kosmos.mediaCarouselController, kosmos.blurConfig, + false, ) } @@ -903,6 +908,20 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + fun onShowPreviousMedia_scrollHandler_isCalled() = + testScope.runTest { + underTest.onShowPreviousMedia() + verify(mediaCarouselScrollHandler).scrollByStep(-1) + } + + @Test + fun onShowNextMedia_scrollHandler_isCalled() = + testScope.runTest { + underTest.onShowNextMedia() + verify(mediaCarouselScrollHandler).scrollByStep(1) + } + + @Test @EnableFlags(FLAG_BOUNCER_UI_REVAMP) fun uiIsBlurred_whenPrimaryBouncerIsShowing() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index e13f3f12c55a..7e93f5a8c9a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -20,19 +20,26 @@ import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.Flags.FLAG_SCENE_CONTAINER +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository @@ -56,11 +63,14 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -93,7 +103,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FLAG_GLANCEABLE_HUB_V2, + ) } } @@ -107,6 +120,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT // Transition to DOZING and set the power interactor asleep. kosmos.powerInteractor.setAsleepForTest() + kosmos.setCommunalV2ConfigEnabled(true) runBlocking { kosmos.transitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, @@ -160,7 +174,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) - @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_GLANCEABLE_HUB_V2) fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() = kosmos.runTest { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) @@ -179,7 +193,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() = kosmos.runTest { setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } clearInvocations(fakeCommunalSceneRepository) powerInteractor.setAwakeForTest() @@ -240,7 +264,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT fun testTransitionToGlanceableHub_onWakeup_ifAvailable() = kosmos.runTest { setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } // Device turns on. powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 8e1068226431..5882cff74eb6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -19,14 +19,20 @@ package com.android.systemui.keyguard.domain.interactor import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -46,6 +52,8 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy @@ -66,7 +74,10 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + FLAG_GLANCEABLE_HUB_V2, + ) .andSceneContainer() } } @@ -101,6 +112,7 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu ) reset(kosmos.transitionRepository) kosmos.setCommunalAvailable(true) + kosmos.setCommunalV2ConfigEnabled(true) } kosmos.underTest.start() } @@ -202,7 +214,17 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu reset(transitionRepository) setCommunalAvailable(true) - whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + if (glanceableHubV2()) { + val user = fakeUserRepository.asMainUser() + fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + 1, + user.id, + ) + batteryRepository.fake.setDevicePluggedIn(true) + } else { + whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) + } // Device wakes up. powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index 47ca4b14a26f..f357d0c80822 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt @@ -69,7 +69,7 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { @Parameters( name = "canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," + - " isSingleShade={3}, isShadeTouchable={4}, isOccluded={6}" + " isSingleShade={3}, isShadeTouchable={4}, isOccluded={5}" ) @JvmStatic fun combinations() = buildList { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt index d073cf1ac9db..c2f0ab92b32b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt @@ -16,8 +16,11 @@ package com.android.systemui.media.controls.ui.view +import android.content.res.Resources import android.testing.TestableLooper import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,16 +28,21 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -42,7 +50,9 @@ import org.mockito.MockitoAnnotations class MediaCarouselScrollHandlerTest : SysuiTestCase() { private val carouselWidth = 1038 + private val settingsButtonWidth = 200 private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) + private lateinit var testableLooper: TestableLooper @Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var pageIndicator: PageIndicator @@ -53,6 +63,9 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { @Mock lateinit var falsingManager: FalsingManager @Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit @Mock lateinit var logger: MediaUiEventLogger + @Mock lateinit var contentContainer: ViewGroup + @Mock lateinit var settingsButton: View + @Mock lateinit var resources: Resources lateinit var executor: FakeExecutor private val clock = FakeSystemClock() @@ -63,6 +76,11 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(clock) + testableLooper = TestableLooper.get(this) + PhysicsAnimatorTestUtils.prepareForTest() + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + + whenever(mediaCarousel.contentContainer).thenReturn(contentContainer) mediaCarouselScrollHandler = MediaCarouselScrollHandler( mediaCarousel, @@ -74,13 +92,17 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { closeGuts, falsingManager, logSmartspaceImpression, - logger + logger, ) mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth - whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener) } + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + @Test fun testCarouselScroll_shortScroll() { whenever(mediaCarousel.isLayoutRtl).thenReturn(false) @@ -128,4 +150,109 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) } + + @Test + fun testCarouselScrollByStep_scrollRight() { + setupMediaContainer(visibleIndex = 0) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft() { + setupMediaContainer(visibleIndex = 1) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) + } + + @Test + fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() { + setupMediaContainer(visibleIndex = 1) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() { + setupMediaContainer(visibleIndex = 0) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() { + setupMediaContainer(visibleIndex = 0) + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + whenever(mediaCarousel.isLayoutRtl).thenReturn(true) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() { + setupMediaContainer(visibleIndex = 1) + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + whenever(mediaCarousel.isLayoutRtl).thenReturn(true) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) + } + + @Test + fun testScrollByStep_noScroll_notDismissible() { + setupMediaContainer(visibleIndex = 1, showsSettingsButton = false) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel, never()).animationTargetX = anyFloat() + } + + private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) { + whenever(contentContainer.childCount).thenReturn(2) + val child1: View = mock() + val child2: View = mock() + whenever(child1.left).thenReturn(0) + whenever(child2.left).thenReturn(carouselWidth) + whenever(contentContainer.getChildAt(0)).thenReturn(child1) + whenever(contentContainer.getChildAt(1)).thenReturn(child2) + + whenever(settingsButton.width).thenReturn(settingsButtonWidth) + whenever(settingsButton.context).thenReturn(context) + whenever(settingsButton.resources).thenReturn(resources) + whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20) + mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton) + + mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex + mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt index 264eda5a07eb..668c606677ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.settings.userFileManager import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository @@ -76,11 +77,11 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { @Test fun setLargeTilesSpecs_inSharedPreferences() { val setA = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setA.toTileSpecs()) + underTest.writeLargeTileSpecs(setA.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA) val setB = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setB.toTileSpecs()) + underTest.writeLargeTileSpecs(setB.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB) } @@ -92,12 +93,12 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) val setA = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setA.toTileSpecs()) + underTest.writeLargeTileSpecs(setA.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA) fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) val setB = setOf("tileA", "tileB") - underTest.setLargeTilesSpecs(setB.toTileSpecs()) + underTest.writeLargeTileSpecs(setB.toTileSpecs()) assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB) fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) @@ -106,7 +107,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } @Test - fun setInitialTilesFromSettings_noLargeTiles_tilesSet() = + fun setUpgradePathFromSettings_noLargeTiles_tilesSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -117,14 +118,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse() - underTest.setInitialLargeTilesSpecs(tiles, PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(tiles) } } @Test - fun setInitialTilesFromSettings_alreadyLargeTiles_tilesNotSet() = + fun setUpgradePathFromSettings_alreadyLargeTiles_tilesNotSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -133,14 +137,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) setLargeTilesSpecsInSharedPreferences(setOf("tileC")) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEqualTo(setOf("tileC").toTileSpecs()) } } @Test - fun setInitialTilesFromSettings_emptyLargeTiles_tilesNotSet() = + fun setUpgradePathFromSettings_emptyLargeTiles_tilesNotSet() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -149,14 +156,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setSelectedUserInfo(ANOTHER_USER) setLargeTilesSpecsInSharedPreferences(emptySet()) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEmpty() } } @Test - fun setInitialTilesFromSettings_nonCurrentUser_tilesSetForCorrectUser() = + fun setUpgradePathFromSettings_nonCurrentUser_tilesSetForCorrectUser() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -164,7 +174,10 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { fakeUserRepository.setUserInfos(USERS) fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) - underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()), + ANOTHER_USER_ID, + ) assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) @@ -174,7 +187,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { } @Test - fun setInitialTiles_afterDefaultRead_noSetOnRepository_initialTilesCorrect() = + fun setUpgradePath_afterDefaultRead_noSetOnRepository_initialTilesCorrect() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -186,14 +199,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(currentLargeTiles).isNotEmpty() val tiles = setOf("tileA", "tileB") - underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(tiles.toTileSpecs()) } } @Test - fun setInitialTiles_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() = + fun setUpgradePath_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() = with(kosmos) { testScope.runTest { val largeTiles by collectLastValue(underTest.largeTilesSpecs) @@ -204,15 +220,80 @@ class QSPreferencesRepositoryTest : SysuiTestCase() { assertThat(currentLargeTiles).isNotEmpty() - underTest.setLargeTilesSpecs(setOf(TileSpec.create("tileC"))) + underTest.writeLargeTileSpecs(setOf(TileSpec.create("tileC"))) val tiles = setOf("tileA", "tileB") - underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID) + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()), + PRIMARY_USER_ID, + ) assertThat(largeTiles).isEqualTo(setOf(TileSpec.create("tileC"))) } } + @Test + fun setTilesRestored_noLargeTiles_tilesSet() = + with(kosmos) { + testScope.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + val tiles = setOf("tileA", "tileB").toTileSpecs() + + assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse() + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.RestoreFromBackup(tiles), + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + } + + @Test + fun setDefaultTilesInitial_defaultSetLarge() = + with(kosmos) { + testScope.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.DefaultSet, + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + } + + @Test + fun setTilesRestored_afterDefaultSet_tilesSet() = + with(kosmos) { + testScope.runTest { + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.DefaultSet, + PRIMARY_USER_ID, + ) + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + fakeUserRepository.setUserInfos(USERS) + fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + val tiles = setOf("tileA", "tileB").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTiles( + TilesUpgradePath.RestoreFromBackup(tiles), + PRIMARY_USER_ID, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + } + private fun getSharedPreferences(): SharedPreferences = with(kosmos) { return userFileManager.getSharedPreferences( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt new file mode 100644 index 000000000000..f3c1f0c9dba8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.panels.domain + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.mainResources +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.common.shared.model.PackageChangeModel.Empty.packageName +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository +import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository +import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor +import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository +import com.android.systemui.qs.pipeline.data.repository.defaultTilesRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath +import com.android.systemui.settings.userFileManager +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.userRepository +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LargeTilesUpgradePathsTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { defaultTilesRepository = DefaultTilesQSHostRepository(mainResources) } + + private val defaultTiles = kosmos.defaultTilesRepository.defaultTiles.toSet() + + private val underTest = kosmos.qsPreferencesInteractor + + private val Kosmos.userId + get() = userRepository.getSelectedUserInfo().id + + private val Kosmos.intent + get() = + Intent(ACTION_RESTORE_FINISHED).apply { + `package` = packageName + putExtra(Intent.EXTRA_USER_ID, kosmos.userId) + flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY + } + + /** + * This test corresponds to the case of a fresh start. + * + * The resulting large tiles are the default set of large tiles. + */ + @Test + fun defaultTiles_noDataInSharedPreferences_defaultLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + + /** + * This test corresponds to a user that upgraded in place from a build that didn't support large + * tiles to one that does. The current tiles of the user are read from settings. + * + * The resulting large tiles are those that were read from Settings. + */ + @Test + fun upgradeInPlace_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a fresh start, and then the user restarts the device, without ever + * having modified the set of large tiles. + * + * The resulting large tiles are the default large tiles that were set on the fresh start + */ + @Test + fun defaultSet_restartDevice_largeTilesDontChange() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + // User restarts the device, this will send a read from settings with the default + // set of tiles + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(defaultTiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles) + } + + /** + * This test corresponds to a fresh start, following the user changing the sizes of some tiles. + * After that, the user restarts the device. + * + * The resulting set of large tiles are those that the user determined before restarting the + * device. + */ + @Test + fun defaultSet_someSizeChanges_restart_correctSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + underTest.setLargeTilesSpecs(largeTiles!! + setOf("a", "b").toTileSpecs()) + val largeTilesBeforeRestart = largeTiles!! + + // Restart + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(defaultTiles), + userId, + ) + assertThat(largeTiles).isEqualTo(largeTilesBeforeRestart) + } + + /** + * This test corresponds to a user that upgraded, and after that performed some size changes. + * After that, the user restarts the device. + * + * The resulting set of large tiles are those that the user determined before restarting the + * device. + */ + @Test + fun readFromSettings_changeSizes_restart_newLargeSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val readTiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + underTest.setLargeTilesSpecs(emptySet()) + + assertThat(largeTiles).isEmpty() + + // Restart + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + assertThat(largeTiles).isEmpty() + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. Note that there's no file in SharedPreferences to + * restore. + * + * The resulting set of large tiles are those that were restored from the backup. + */ + @Test + fun restoreFromBackup_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. However, the restore happens after SystemUI's + * initialization has set the tiles to default. Note that there's no file in SharedPreferences + * to restore. + * + * The resulting set of large tiles are those that were restored from the backup. + */ + @Test + fun restoreFromBackup_afterDefault_noDataInSharedPreferences_allLargeTiles() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId) + + val tiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tiles) + } + + /** + * This test corresponds to a user that restored from a build that supported different sizes + * tiles. First the list of tiles is restored in Settings and then a file containing some large + * tiles overrides the current shared preferences file + * + * The resulting set of large tiles are those that were restored from the shared preferences + * backup (and not the full list). + */ + @Test + fun restoreFromBackup_thenRestoreOfSharedPrefs_sharedPrefsAreLarge() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + val tilesFromBackupOfSharedPrefs = setOf("a") + setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs()) + } + + /** + * This test corresponds to a user that restored from a build that supported different sizes + * tiles. However, this restore of settings happened after SystemUI's restore of the SharedPrefs + * containing the user's previous selections to large/small tiles. + * + * The resulting set of large tiles are those that were restored from the shared preferences + * backup (and not the full list). + */ + @Test + fun restoreFromBackup_afterRestoreOfSharedPrefs_sharedPrefsAreLarge() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val tiles = setOf("a", "b", "c").toTileSpecs() + val tilesFromBackupOfSharedPrefs = setOf("a") + + setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(tiles), + userId, + ) + + assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs()) + } + + /** + * This test corresponds to a user that upgraded from a build that didn't support tile sizes to + * one that does, via restore from backup. After that, the user modifies the size of some tiles + * and then restarts the device. + * + * The resulting set of large tiles are those after the user modifications. + */ + @Test + fun restoreFromBackup_changeSizes_restart_newLargeSet() = + kosmos.runTest { + val largeTiles by collectLastValue(underTest.largeTilesSpecs) + val readTiles = setOf("a", "b", "c").toTileSpecs() + + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.RestoreFromBackup(readTiles), + userId, + ) + underTest.setLargeTilesSpecs(emptySet()) + + assertThat(largeTiles).isEmpty() + + // Restart + underTest.setInitialOrUpgradeLargeTilesSpecs( + TilesUpgradePath.ReadFromSettings(readTiles), + userId, + ) + assertThat(largeTiles).isEmpty() + } + + private companion object { + private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" + + private fun Kosmos.getSharedPreferences(): SharedPreferences = + userFileManager.getSharedPreferences( + QSPreferencesRepository.FILE_NAME, + Context.MODE_PRIVATE, + userRepository.getSelectedUserInfo().id, + ) + + private fun Kosmos.setLargeTilesSpecsInSharedPreferences(specs: Set<String>) { + getSharedPreferences().edit().putStringSet(LARGE_TILES_SPECS_KEY, specs).apply() + } + + private fun Kosmos.getLargeTilesSpecsFromSharedPreferences(): Set<String> { + return getSharedPreferences().getStringSet(LARGE_TILES_SPECS_KEY, emptySet())!! + } + + private fun Set<String>.toTileSpecs(): Set<TileSpec> { + return map { TileSpec.create(it) }.toSet() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt index 79acfdaa415b..9838bcb86684 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt @@ -66,7 +66,7 @@ class IconTilesInteractorTest : SysuiTestCase() { runCurrent() // Resize it to large - qsPreferencesRepository.setLargeTilesSpecs(setOf(spec)) + qsPreferencesRepository.writeLargeTileSpecs(setOf(spec)) runCurrent() // Assert that the new tile was added to the large tiles set diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index 4b8cd3742bff..d9b3926fa215 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.res.R import com.android.systemui.retail.data.repository.FakeRetailModeRepository @@ -242,9 +243,12 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { storeTilesForUser(startingTiles, userId) val tiles by collectLastValue(underTest.tilesSpecs(userId)) - val tilesRead by collectLastValue(underTest.tilesReadFromSetting.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) - assertThat(tilesRead).isEqualTo(startingTiles.toTileSpecs().toSet() to userId) + assertThat(tilesRead) + .isEqualTo( + TilesUpgradePath.ReadFromSettings(startingTiles.toTileSpecs().toSet()) to userId + ) } @Test @@ -258,13 +262,13 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { val tiles10 by collectLastValue(underTest.tilesSpecs(10)) val tiles11 by collectLastValue(underTest.tilesSpecs(11)) - val tilesRead by collectValues(underTest.tilesReadFromSetting.consumeAsFlow()) + val tilesRead by collectValues(underTest.tilesUpgradePath.consumeAsFlow()) assertThat(tilesRead).hasSize(2) assertThat(tilesRead) .containsExactly( - startingTiles10.toTileSpecs().toSet() to 10, - startingTiles11.toTileSpecs().toSet() to 11, + TilesUpgradePath.ReadFromSettings(startingTiles10.toTileSpecs().toSet()) to 10, + TilesUpgradePath.ReadFromSettings(startingTiles11.toTileSpecs().toSet()) to 11, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt index 1945f750efaf..29bd18d3f3a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt @@ -7,8 +7,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.pipeline.data.model.RestoreData -import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepositoryTest.Companion.toTilesSet import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat @@ -352,11 +352,11 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { @Test fun noSettingsStored_noTilesReadFromSettings() = testScope.runTest { - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) val tiles by collectLastValue(underTest.tiles()) assertThat(tiles).isEqualTo(getDefaultTileSpecs()) - assertThat(tilesRead).isEqualTo(null) + assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet) } @Test @@ -365,19 +365,20 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { val storedTiles = "a,b" storeTiles(storedTiles) val tiles by collectLastValue(underTest.tiles()) - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) - assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet()) + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet())) } @Test fun noSettingsStored_tilesChanged_tilesReadFromSettingsNotChanged() = testScope.runTest { - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) val tiles by collectLastValue(underTest.tiles()) underTest.addTile(TileSpec.create("a")) - assertThat(tilesRead).isEqualTo(null) + assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet) } @Test @@ -386,10 +387,34 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { val storedTiles = "a,b" storeTiles(storedTiles) val tiles by collectLastValue(underTest.tiles()) - val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) underTest.addTile(TileSpec.create("c")) - assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet()) + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet())) + } + + @Test + fun tilesRestoredFromBackup() = + testScope.runTest { + val specsBeforeRestore = "a,b,c,d,e" + val restoredSpecs = "a,c,d,f" + val autoAddedBeforeRestore = "b,d" + val restoredAutoAdded = "d,e" + + storeTiles(specsBeforeRestore) + val tiles by collectLastValue(underTest.tiles()) + val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow()) + runCurrent() + + val restoreData = + RestoreData(restoredSpecs.toTileSpecs(), restoredAutoAdded.toTilesSet(), USER) + underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet()) + runCurrent() + + val expected = "a,b,c,d,f" + assertThat(tilesRead) + .isEqualTo(TilesUpgradePath.RestoreFromBackup(expected.toTilesSet())) } private fun getDefaultTileSpecs(): List<TileSpec> { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt index c1477fe52f0e..a9f3a655ada9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt @@ -80,6 +80,8 @@ class WorkProfilePolicyTest { // Set desktop mode supported whenever(mContext.resources).thenReturn(mResources) whenever(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true) + whenever(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops)) + .thenReturn(true) policy = WorkProfilePolicy(kosmos.profileTypeRepository, mContext) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index dbe8f8226d43..c7b3175a636f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -31,7 +31,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer @@ -265,91 +264,25 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - fun chip_positiveStartTime_notPromoted_colorsAreThemed() = + fun chip_positiveStartTime_colorsAreAccentThemed() = testScope.runTest { val latest by collectLastValue(underTest.chip) repo.setOngoingCallState(inCallModel(startTimeMs = 1000, promotedContent = null)) assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) + .isEqualTo(ColorsModel.AccentThemed) } @Test - fun chip_zeroStartTime_notPromoted_colorsAreThemed() = + fun chip_zeroStartTime_colorsAreAccentThemed() = testScope.runTest { val latest by collectLastValue(underTest.chip) repo.setOngoingCallState(inCallModel(startTimeMs = 0, promotedContent = null)) assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) - } - - @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_positiveStartTime_promoted_notifChipsFlagOff_colorsAreThemed() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) - } - - @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_zeroStartTime_promoted_notifChipsFlagOff_colorsAreThemed() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo(ColorsModel.Themed) - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_positiveStartTime_promoted_notifChipsFlagOn_colorsAreCustom() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo( - ColorsModel.Custom( - backgroundColorInt = PROMOTED_BACKGROUND_COLOR, - primaryTextColorInt = PROMOTED_PRIMARY_TEXT_COLOR, - ) - ) - } - - @Test - @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun chip_zeroStartTime_promoted_notifChipsFlagOff_colorsAreCustom() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, promotedContent = PROMOTED_CONTENT_WITH_COLOR) - ) - - assertThat((latest as OngoingActivityChipModel.Active).colors) - .isEqualTo( - ColorsModel.Custom( - backgroundColorInt = PROMOTED_BACKGROUND_COLOR, - primaryTextColorInt = PROMOTED_PRIMARY_TEXT_COLOR, - ) - ) + .isEqualTo(ColorsModel.AccentThemed) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 192ad879891f..aaa9b58a45df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -186,7 +186,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_onePromotedNotif_colorMatches() = + fun chips_onePromotedNotif_colorIsSystemThemed() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -209,10 +209,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) assertThat(latest).hasSize(1) - val colors = latest!![0].colors - assertThat(colors).isInstanceOf(ColorsModel.Custom::class.java) - assertThat((colors as ColorsModel.Custom).backgroundColorInt).isEqualTo(56) - assertThat((colors as ColorsModel.Custom).primaryTextColorInt).isEqualTo(89) + assertThat(latest!![0].colors).isEqualTo(ColorsModel.SystemThemed) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt index d727089094f0..9ec5a42714bf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelperTest.kt @@ -52,24 +52,13 @@ class ChipTextTruncationHelperTest : SysuiTestCase() { } @Test - fun shouldShowText_desiredSlightlyLargerThanMax_true() { + fun shouldShowText_desiredMoreThanMax_false() { val result = underTest.shouldShowText( desiredTextWidthPx = (MAX_WIDTH * 1.1).toInt(), widthMeasureSpec = UNLIMITED_WIDTH_SPEC, ) - assertThat(result).isTrue() - } - - @Test - fun shouldShowText_desiredMoreThanTwiceMax_false() { - val result = - underTest.shouldShowText( - desiredTextWidthPx = (MAX_WIDTH * 2.2).toInt(), - widthMeasureSpec = UNLIMITED_WIDTH_SPEC, - ) - assertThat(result).isFalse() } @@ -80,8 +69,8 @@ class ChipTextTruncationHelperTest : SysuiTestCase() { View.MeasureSpec.makeMeasureSpec(MAX_WIDTH / 2, View.MeasureSpec.AT_MOST) ) - // WHEN desired is more than twice the smallerWidthSpec - val desiredWidth = (MAX_WIDTH * 1.1).toInt() + // WHEN desired is more than the smallerWidthSpec + val desiredWidth = ((MAX_WIDTH / 2) * 1.1).toInt() val result = underTest.shouldShowText( @@ -100,8 +89,8 @@ class ChipTextTruncationHelperTest : SysuiTestCase() { View.MeasureSpec.makeMeasureSpec(MAX_WIDTH * 3, View.MeasureSpec.AT_MOST) ) - // WHEN desired is more than twice the max - val desiredWidth = (MAX_WIDTH * 2.2).toInt() + // WHEN desired is more than the max + val desiredWidth = (MAX_WIDTH * 1.1).toInt() val result = underTest.shouldShowText( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt index 60030ad4e428..e3a84fd2c2eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt @@ -54,7 +54,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.Timer( key = KEY, icon = createIcon(R.drawable.ic_cake), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, startTimeMs = 100L, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, @@ -68,7 +68,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.IconOnly( key = KEY, icon = createIcon(R.drawable.ic_hotspot), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, ) @@ -90,7 +90,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.Timer( key = KEY, icon = createIcon(R.drawable.ic_cake), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, startTimeMs = 100L, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, @@ -132,7 +132,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { OngoingActivityChipModel.Active.Timer( key = KEY, icon = createIcon(R.drawable.ic_cake), - colors = ColorsModel.Themed, + colors = ColorsModel.AccentThemed, startTimeMs = 100L, onClickListenerLegacy = null, clickBehavior = OngoingActivityChipModel.ClickBehavior.None, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 403ac3288128..20637cd4af33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -293,7 +293,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test - fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() = + fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording addOngoingCallState(key = "call") @@ -307,6 +307,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_twoTimerChips_isSmallPortrait_bothSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + val latest by collectLastValue(underTest.chips) + + // Squished chips are icon only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = @@ -324,6 +340,23 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) + addOngoingCallState(key = "call") + + val latest by collectLastValue(underTest.chips) + + // The screen record countdown isn't squished to icon-only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java) + // But the call chip *is* squished + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = @@ -360,6 +393,38 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + // WHEN there's only one chip + screenRecordState.value = ScreenRecordModel.Recording + removeOngoingCallState(key = "call") + + // The screen record isn't squished because it's the only one + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + + // WHEN there's 2 chips + addOngoingCallState(key = "call") + + // THEN they both become squished + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + // But the call chip *is* squished + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + + // WHEN we go back down to 1 chip + screenRecordState.value = ScreenRecordModel.DoingNothing + + // THEN the remaining chip unsquishes + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_twoChips_isLandscape_notSquished() = @@ -383,6 +448,29 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } + @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + @Test + fun chips_twoChips_isLandscape_notSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + // WHEN we're in landscape + val config = + Configuration(kosmos.mainResources.configuration).apply { + orientation = Configuration.ORIENTATION_LANDSCAPE + } + kosmos.fakeConfigurationRepository.onConfigurationChange(config) + + val latest by collectLastValue(underTest.chips) + + // THEN the chips aren't squished (squished chips would be icon only) + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + } + @DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) @Test fun chipsLegacy_twoChips_isLargeScreen_notSquished() = @@ -402,16 +490,19 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) } - @Test @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun chips_twoChips_chipsModernizationEnabled_notSquished() = + @Test + fun chips_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording addOngoingCallState(key = "call") + // WHEN we're on a large screen + kosmos.displayStateRepository.setIsLargeScreen(true) + val latest by collectLastValue(underTest.chips) - // Squished chips would be icon only + // THEN the chips aren't squished (squished chips would be icon only) assertThat(latest!!.active[0]) .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat(latest!!.active[1]) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index 57b7df7a8d31..31f8590c0378 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -51,11 +51,13 @@ import com.android.systemui.res.R import com.android.systemui.shade.ShadeViewStateProvider import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.batteryViewModelFactory import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController @@ -85,6 +87,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) +@DisableFlags(NewStatusBarIcons.FLAG_NAME) class KeyguardStatusBarViewControllerTest : SysuiTestCase() { private lateinit var kosmos: Kosmos private lateinit var testScope: TestScope @@ -190,6 +193,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { statusBarIconController, iconManagerFactory, batteryMeterViewController, + kosmos.batteryViewModelFactory, shadeViewStateProvider, keyguardStateController, keyguardBypassController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index 4759c081de5f..183cd8f1ae8b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.graphics.Color import android.graphics.Rect import android.view.View +import androidx.compose.runtime.getValue +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel @@ -26,15 +29,20 @@ import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChip import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel import com.android.systemui.statusbar.pipeline.shared.ui.model.SystemInfoCombinedVisibilityModel import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import org.mockito.Mockito.mock class FakeHomeStatusBarViewModel( override val operatorNameViewModel: StatusBarOperatorNameViewModel -) : HomeStatusBarViewModel { +) : HomeStatusBarViewModel, ExclusiveActivatable() { + private val hydrator = Hydrator("FakeHomeStatusBarViewModel.hydrator") + override val areNotificationsLightsOut = MutableStateFlow(false) override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false) @@ -56,6 +64,11 @@ class FakeHomeStatusBarViewModel( override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) + override val batteryViewModelFactory: BatteryViewModel.Factory = + object : BatteryViewModel.Factory { + override fun create(): BatteryViewModel = mock(BatteryViewModel::class.java) + } + override val shouldShowOperatorNameView = MutableStateFlow(false) override val isClockVisible = @@ -80,6 +93,7 @@ class FakeHomeStatusBarViewModel( var darkIconTint = Color.BLACK var lightIconTint = Color.WHITE + var darkIntensity = 0f override val areaTint: Flow<StatusBarTintColor> = MutableStateFlow( @@ -91,4 +105,22 @@ class FakeHomeStatusBarViewModel( } } ) + + val isAreaDarkSource = + MutableStateFlow( + IsAreaDark { viewBounds -> + if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) { + darkIntensity < 0.5f + } else { + false + } + } + ) + + override val areaDark: IsAreaDark by + hydrator.hydratedStateOf(traceName = "areaDark", source = isAreaDarkSource) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index 354edac75452..36e18e653f20 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -56,7 +56,6 @@ import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImp import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.unfoldedDeviceState import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -79,8 +78,10 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.times +import org.mockito.kotlin.verifyNoMoreInteractions @RunWith(AndroidJUnit4::class) @SmallTest @@ -603,12 +604,39 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { } } + @Test + fun foldingStarted_screenStillOn_eventSentOnlyAfterScreenSwitches() { + // can happen for both folding and unfolding (with animations off) but it's more likely to + // happen when folding as waiting for screen on is the default case then + testScope.runTest { + startInUnfoldedState(displaySwitchLatencyTracker) + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verifyNoMoreInteractions(displaySwitchLatencyLogger) + + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger).log(any()) + } + } + private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) { setDeviceState(FOLDED) tracker.start() runCurrent() } + private suspend fun TestScope.startInUnfoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(UNFOLDED) + tracker.start() + runCurrent() + } + private suspend fun TestScope.startUnfolding() { setDeviceState(HALF_FOLDED) powerInteractor.setScreenPowerState(SCREEN_OFF) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index 75f3386ed695..b8e19248b2de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -47,6 +47,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.keyguard.TestScopeProvider; +import com.android.settingslib.volume.MediaSessions; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCaseExtKt; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -268,13 +269,15 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { @Test public void testOnRemoteVolumeChanged_newStream_noNullPointer() { MediaSession.Token token = new MediaSession.Token(Process.myUid(), null); - mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(token, 0); + var sessionId = MediaSessions.SessionId.Companion.from(token); + mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(sessionId, 0); } @Test public void testOnRemoteRemove_newStream_noNullPointer() { MediaSession.Token token = new MediaSession.Token(Process.myUid(), null); - mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(token); + var sessionId = MediaSessions.SessionId.Companion.from(token); + mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(sessionId); } @Test diff --git a/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml new file mode 100644 index 000000000000..1ba637f379c1 --- /dev/null +++ b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<inset + xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/clipboard_minimized_background" + android:inset="4dp"/>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 448b3e7d5ea0..915563b1ae20 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -171,12 +171,12 @@ android:layout_height="wrap_content" android:visibility="gone" android:elevation="7dp" - android:padding="8dp" + android:padding="12dp" app:layout_constraintBottom_toTopOf="@id/indication_container" app:layout_constraintStart_toStartOf="parent" - android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" - android:background="@drawable/clipboard_minimized_background"> + android:layout_marginStart="4dp" + android:layout_marginBottom="2dp" + android:background="@drawable/clipboard_minimized_background_inset"> <ImageView android:src="@drawable/ic_content_paste" android:tint="?attr/overlayButtonTextColor" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index b9ef88eea6b9..32407c6ae1bf 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -147,6 +147,7 @@ frame when animating QS <-> QQS transition android:id="@+id/batteryRemainingIcon" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:visibility="gone" app:textAppearance="@style/TextAppearance.QS.Status" /> </LinearLayout> </FrameLayout> diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml b/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml index 6f42286d9fac..b66a88a3e523 100644 --- a/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml +++ b/packages/SystemUI/res/layout/ongoing_activity_chip_content.xml @@ -43,9 +43,6 @@ ongoing_activity_chip_short_time_delta] will ever be shown at one time. --> <!-- Shows a timer, like 00:01. --> - <!-- Don't use the LimitedWidth style for the timer because the end of the timer is often - the most important value. ChipChronometer has the correct logic for when the timer is - too large for the space allowed. --> <com.android.systemui.statusbar.chips.ui.view.ChipChronometer android:id="@+id/ongoing_activity_chip_time" style="@style/StatusBar.Chip.Text" @@ -54,14 +51,14 @@ <!-- Shows generic text. --> <com.android.systemui.statusbar.chips.ui.view.ChipTextView android:id="@+id/ongoing_activity_chip_text" - style="@style/StatusBar.Chip.Text.LimitedWidth" + style="@style/StatusBar.Chip.Text" android:visibility="gone" /> <!-- Shows a time delta in short form, like "15min" or "1hr". --> <com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView android:id="@+id/ongoing_activity_chip_short_time_delta" - style="@style/StatusBar.Chip.Text.LimitedWidth" + style="@style/StatusBar.Chip.Text" android:visibility="gone" /> diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml index c28dc50cc1dc..bb99d581c0b0 100644 --- a/packages/SystemUI/res/layout/system_icons.xml +++ b/packages/SystemUI/res/layout/system_icons.xml @@ -34,12 +34,17 @@ android:orientation="horizontal"/> <!-- PaddingEnd is added to balance hover padding, compensating for paddingStart in statusIcons. - See b/339589733 --> + See b/339589733. + + Default visibility is now "gone" to make space for the new battery icon + --> <com.android.systemui.battery.BatteryMeterView android:id="@+id/battery" android:layout_height="wrap_content" android:layout_width="wrap_content" android:clipToPadding="false" android:clipChildren="false" android:paddingEnd="@dimen/status_bar_battery_end_padding" + android:visibility="gone" systemui:textAppearance="@style/TextAppearance.StatusBar.Clock" /> + </LinearLayout> diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 67f620f6fc54..8ad99abccdfe 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -16,7 +16,7 @@ <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/volume_dialog_root" + android:id="@+id/volume_dialog" android:layout_width="match_parent" android:layout_height="match_parent" android:alpha="0" diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index 6748cfa05c35..4e3c8cc4413b 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -13,20 +13,13 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<ImageButton xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" > - - <ImageButton - android:id="@+id/volume_drawer_button" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:contentDescription="@string/volume_ringer_mode" - android:gravity="center" - android:tint="@androidprv:color/materialColorOnSurface" - android:src="@drawable/volume_ringer_item_bg" - android:background="@drawable/volume_ringer_item_bg"/> - -</FrameLayout> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/volume_ringer_item_bg" + android:contentDescription="@string/volume_ringer_mode" + android:gravity="center" + android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" + android:src="@drawable/volume_ringer_item_bg" + android:tint="@androidprv:color/materialColorOnSurface" /> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9b8926e921c9..09aa2241e42b 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1110,4 +1110,7 @@ <!-- Configuration for wallpaper focal area --> <bool name="center_align_focal_area_shape">false</bool> <string name="focal_area_target" translatable="false" /> + + <!-- Configuration to swipe to open glanceable hub --> + <bool name="config_swipeToOpenGlanceableHub">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2d3c07b93cb1..648e4c2e3ac7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1811,6 +1811,7 @@ <dimen name="ongoing_activity_chip_text_end_padding_for_embedded_padding_icon">6dp</dimen> <dimen name="ongoing_activity_chip_text_fading_edge_length">12dp</dimen> <dimen name="ongoing_activity_chip_corner_radius">28dp</dimen> + <dimen name="ongoing_activity_chip_outline_width">2px</dimen> <!-- Status bar user chip --> <dimen name="status_bar_user_chip_avatar_size">16dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d18a90a17abe..86292039d93d 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1351,6 +1351,10 @@ <string name="accessibility_action_label_shrink_widget">Decrease height</string> <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] --> <string name="accessibility_action_label_expand_widget">Increase height</string> + <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_umo_show_next">Show next</string> + <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_umo_show_previous">Show previous</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 7f2c89346423..4961a7ece69a 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -93,15 +93,6 @@ <item name="android:textColor">?android:attr/colorPrimary</item> </style> - <!-- Style for a status bar chip text that has a maximum width. Since there's so little room in - the status bar chip area, don't ellipsize the text and instead just fade it out a bit at - the end. --> - <style name="StatusBar.Chip.Text.LimitedWidth"> - <item name="android:ellipsize">none</item> - <item name="android:requiresFadingEdge">horizontal</item> - <item name="android:fadingEdgeLength">@dimen/ongoing_activity_chip_text_fading_edge_length</item> - </style> - <style name="Chipbar" /> <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title"> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 335a910eb106..73dc28230e65 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -98,7 +98,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.settingslib.Utils; import com.android.settingslib.drawable.CircleFramedDrawable; -import com.android.systemui.Flags; import com.android.systemui.FontStyles; import com.android.systemui.Gefingerpoken; import com.android.systemui.classifier.FalsingA11yDelegate; @@ -121,6 +120,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { static final int USER_TYPE_PRIMARY = 1; static final int USER_TYPE_WORK_PROFILE = 2; static final int USER_TYPE_SECONDARY_USER = 3; + private boolean mTransparentModeEnabled = false; @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER}) public @interface Mode {} @@ -814,15 +814,30 @@ public class KeyguardSecurityContainer extends ConstraintLayout { mDisappearAnimRunning = false; } + /** + * Make the bouncer background transparent + */ + public void enableTransparentMode() { + mTransparentModeEnabled = true; + reloadBackgroundColor(); + } + + /** + * Make the bouncer background opaque + */ + public void disableTransparentMode() { + mTransparentModeEnabled = false; + reloadBackgroundColor(); + } + private void reloadBackgroundColor() { - if (Flags.bouncerUiRevamp()) { - // Keep the background transparent, otherwise the background color looks like a box - // while scaling the bouncer for back animation or while transitioning to the bouncer. + if (mTransparentModeEnabled) { setBackgroundColor(Color.TRANSPARENT); } else { setBackgroundColor( getContext().getColor(com.android.internal.R.color.materialColorSurfaceDim)); } + invalidate(); } void reloadColors() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index ff7b2b025539..d10fce416150 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -70,6 +70,7 @@ import com.android.keyguard.KeyguardSecurityContainer.SwipeListener; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.keyguard.dagger.KeyguardBouncerScope; import com.android.settingslib.utils.ThreadUtils; +import com.android.systemui.Flags; import com.android.systemui.Gefingerpoken; import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; @@ -96,6 +97,8 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository; +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor; import dagger.Lazy; @@ -134,6 +137,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final FalsingA11yDelegate mFalsingA11yDelegate; private final DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; private final BouncerMessageInteractor mBouncerMessageInteractor; + private final Lazy<WindowRootViewBlurInteractor> mRootViewBlurInteractor; private int mTranslationY; private final KeyguardDismissTransitionInteractor mKeyguardDismissTransitionInteractor; private final DevicePolicyManager mDevicePolicyManager; @@ -431,6 +435,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final Executor mBgExecutor; @Nullable private Job mSceneTransitionCollectionJob; + private Job mBlurEnabledCollectionJob; @Inject public KeyguardSecurityContainerController(KeyguardSecurityContainer view, @@ -463,9 +468,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard KeyguardDismissTransitionInteractor keyguardDismissTransitionInteractor, Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor, @Background Executor bgExecutor, - Provider<DeviceEntryInteractor> deviceEntryInteractor + Provider<DeviceEntryInteractor> deviceEntryInteractor, + Lazy<WindowRootViewBlurInteractor> rootViewBlurInteractorProvider ) { super(view); + mRootViewBlurInteractor = rootViewBlurInteractorProvider; view.setAccessibilityDelegate(faceAuthAccessibilityDelegate); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = keyguardUpdateMonitor; @@ -539,6 +546,32 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } ); } + + if (Flags.bouncerUiRevamp()) { + mBlurEnabledCollectionJob = mJavaAdapter.get().alwaysCollectFlow( + mRootViewBlurInteractor.get().isBlurCurrentlySupported(), + this::handleBlurSupportedChanged); + } + } + + private void handleBlurSupportedChanged(boolean isWindowBlurSupported) { + if (isWindowBlurSupported) { + mView.enableTransparentMode(); + } else { + mView.disableTransparentMode(); + } + } + + private void refreshBouncerBackground() { + // This is present solely for screenshot tests that disable blur by invoking setprop to + // disable blurs, however the mRootViewBlurInteractor#isBlurCurrentlySupported doesn't emit + // an updated value because sysui doesn't have a way to register for changes to setprop. + // KeyguardSecurityContainer view is inflated only once and doesn't re-inflate so it has to + // check the sysprop every time bouncer is about to be shown. + if (Flags.bouncerUiRevamp() && (ActivityManager.isRunningInUserTestHarness() + || ActivityManager.isRunningInTestHarness())) { + handleBlurSupportedChanged(!WindowRootViewBlurRepository.isDisableBlurSysPropSet()); + } } @Override @@ -552,6 +585,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mSceneTransitionCollectionJob.cancel(null); mSceneTransitionCollectionJob = null; } + + if (mBlurEnabledCollectionJob != null) { + mBlurEnabledCollectionJob.cancel(null); + mBlurEnabledCollectionJob = null; + } } /** */ @@ -718,6 +756,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard if (bouncerUserSwitcher != null) { bouncerUserSwitcher.setAlpha(0f); } + + refreshBouncerBackground(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index a67ec65cceda..8734d05bc894 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -296,6 +296,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mGestureDetector = new MagnificationGestureDetector(mContext, handler, this); mWindowInsetChangeRunnable = this::onWindowInsetChanged; + mWindowInsetChangeRunnable.run(); // Initialize listeners. mMirrorViewRunnable = new Runnable() { @@ -367,8 +368,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private boolean updateSystemGestureInsetsTop() { final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics(); final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures()); - final int gestureTop = - insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1; + final int gestureTop; + if (Flags.updateWindowMagnifierBottomBoundary()) { + gestureTop = windowMetrics.getBounds().bottom - insets.bottom; + } else { + gestureTop = insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1; + } if (gestureTop != mSystemGestureTop) { mSystemGestureTop = gestureTop; return true; @@ -953,7 +958,6 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold ? mSystemGestureTop - height + mOuterBorderSize : mWindowBounds.bottom - height + mOuterBorderSize; final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY); - if (computeWindowSize) { LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); params.width = width; diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java index 9a30c213a2f9..fcf51051940c 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java @@ -46,7 +46,10 @@ import java.io.PrintWriter; import javax.inject.Inject; -/** Controller for {@link BatteryMeterView}. **/ +/** + * Controller for {@link BatteryMeterView}. + * @deprecated once [NewStatusBarIcons] is rolled out, this class is no longer needed + */ public class BatteryMeterViewController extends ViewController<BatteryMeterView> { private final ConfigurationController mConfigurationController; private final TunerService mTunerService; diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index f01a6dbf568f..ff741625a3cc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -104,6 +104,7 @@ interface CommunalModule { companion object { const val LOGGABLE_PREFIXES = "loggable_prefixes" const val LAUNCHER_PACKAGE = "launcher_package" + const val SWIPE_TO_HUB = "swipe_to_hub" @Provides @Communal @@ -143,5 +144,11 @@ interface CommunalModule { fun provideLauncherPackage(@Main resources: Resources): String { return resources.getString(R.string.launcher_overlayable_package) } + + @Provides + @Named(SWIPE_TO_HUB) + fun provideSwipeToHub(@Main resources: Resources): Boolean { + return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 49003a735fbd..a4860dfc47ce 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -202,6 +202,12 @@ abstract class BaseCommunalViewModel( /** Called as the user request to show the customize widget button. */ open fun onLongClick() {} + /** Called as the user requests to switch to the previous player in UMO. */ + open fun onShowPreviousMedia() {} + + /** Called as the user requests to switch to the next player in UMO. */ + open fun onShowNextMedia() {} + /** Called as the UI determines that a new widget has been added to the grid. */ open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 4bc44005d2fc..2169881d11c5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.communal.ui.viewmodel import android.content.ComponentName import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags +import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor @@ -92,6 +93,7 @@ constructor( private val metricsLogger: CommunalMetricsLogger, mediaCarouselController: MediaCarouselController, blurConfig: BlurConfig, + @Named(SWIPE_TO_HUB) private val swipeToHub: Boolean, ) : BaseCommunalViewModel( communalSceneInteractor, @@ -254,6 +256,14 @@ constructor( } } + override fun onShowPreviousMedia() { + mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1) + } + + override fun onShowNextMedia() { + mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1) + } + override fun onTapWidget(componentName: ComponentName, rank: Int) { metricsLogger.logTapWidget(componentName.flattenToString(), rank) } @@ -349,6 +359,8 @@ constructor( /** See [CommunalSettingsInteractor.isV2FlagEnabled] */ fun v2FlagEnabled(): Boolean = communalSettingsInteractor.isV2FlagEnabled() + fun swipeToHubEnabled(): Boolean = swipeToHub + companion object { const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 3c68e3a09f02..8bff090959ab 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -74,6 +74,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule; +import com.android.systemui.statusbar.notification.dagger.NotificationStackModule; import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule; import com.android.systemui.statusbar.notification.headsup.HeadsUpModule; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -169,6 +170,7 @@ import javax.inject.Named; WallpaperModule.class, ShortcutHelperModule.class, ContextualEducationModule.class, + NotificationStackModule.class, }) public abstract class ReferenceSystemUIModule { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index f85a23c1f091..eb96c921c181 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -24,6 +24,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -57,6 +58,7 @@ constructor( keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryInteractor: DeviceEntryInteractor, @@ -116,6 +118,17 @@ constructor( } } + @SuppressLint("MissingPermission") + private fun shouldTransitionToCommunal( + shouldShowCommunal: Boolean, + isCommunalAvailable: Boolean, + ) = + if (communalSettingsInteractor.isV2FlagEnabled()) { + shouldShowCommunal + } else { + isCommunalAvailable && dreamManager.canStartDreaming(false) + } + @OptIn(FlowPreview::class) @SuppressLint("MissingPermission") private fun listenForDozingToDreaming() { @@ -141,9 +154,10 @@ constructor( .filterRelevantKeyguardStateAnd { isAwake -> isAwake } .sample( communalInteractor.isCommunalAvailable, + communalInteractor.shouldShowCommunal, communalSceneInteractor.isIdleOnCommunal, ) - .collect { (_, isCommunalAvailable, isIdleOnCommunal) -> + .collect { (_, isCommunalAvailable, shouldShowCommunal, isIdleOnCommunal) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value @@ -177,11 +191,9 @@ constructor( if (!SceneContainerFlag.isEnabled) { startTransitionTo(KeyguardState.GLANCEABLE_HUB) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { - // Using false for isScreenOn as canStartDreaming returns false if any - // dream, including doze, is active. - // This case handles tapping the power button to transition through - // dream -> off -> hub. + } else if ( + shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } @@ -203,6 +215,7 @@ constructor( powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } .sample( + communalInteractor.shouldShowCommunal, communalInteractor.isCommunalAvailable, communalSceneInteractor.isIdleOnCommunal, keyguardInteractor.biometricUnlockState, @@ -212,6 +225,7 @@ constructor( .collect { ( _, + shouldShowCommunal, isCommunalAvailable, isIdleOnCommunal, biometricUnlockState, @@ -245,7 +259,9 @@ constructor( ownerReason = "waking from dozing", ) } - } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) { + } else if ( + shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable) + ) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 251af11f7fe6..c1c509b8fd57 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -129,20 +129,37 @@ constructor( if (!communalSettingsInteractor.isCommunalFlagEnabled()) return if (SceneContainerFlag.isEnabled) return scope.launch { - powerInteractor.isAwake - .debounce(50L) - .filterRelevantKeyguardStateAnd { isAwake -> isAwake } - .sample(communalInteractor.isCommunalAvailable) - .collect { isCommunalAvailable -> - if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { - // This case handles tapping the power button to transition through - // dream -> off -> hub. - communalSceneInteractor.snapToScene( - newScene = CommunalScenes.Communal, - loggingReason = "from dreaming to hub", - ) + if (communalSettingsInteractor.isV2FlagEnabled()) { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.shouldShowCommunal) + .collect { shouldShowCommunal -> + if (shouldShowCommunal) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } } - } + } else { + powerInteractor.isAwake + .debounce(50L) + .filterRelevantKeyguardStateAnd { isAwake -> isAwake } + .sample(communalInteractor.isCommunalAvailable) + .collect { isCommunalAvailable -> + if (isCommunalAvailable && dreamManager.canStartDreaming(false)) { + // This case handles tapping the power button to transition through + // dream -> off -> hub. + communalSceneInteractor.snapToScene( + newScene = CommunalScenes.Communal, + loggingReason = "from dreaming to hub", + ) + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt index d63c2e07b94f..0107a5278e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt @@ -23,11 +23,11 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider +import androidx.annotation.VisibleForTesting import androidx.core.view.GestureDetectorCompat import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce import com.android.app.tracing.TraceStateLogger -import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils import com.android.systemui.Gefingerpoken import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS @@ -38,9 +38,10 @@ import com.android.systemui.res.R import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.DelayableExecutor import com.android.wm.shell.shared.animation.PhysicsAnimator +import kotlin.math.sign private const val FLING_SLOP = 1000000 -private const val DISMISS_DELAY = 100L +@VisibleForTesting const val DISMISS_DELAY = 100L private const val SCROLL_DELAY = 100L private const val RUBBERBAND_FACTOR = 0.2f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f @@ -64,7 +65,7 @@ class MediaCarouselScrollHandler( private val closeGuts: (immediate: Boolean) -> Unit, private val falsingManager: FalsingManager, private val logSmartspaceImpression: (Boolean) -> Unit, - private val logger: MediaUiEventLogger + private val logger: MediaUiEventLogger, ) { /** Trace state logger for media carousel visibility */ private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser") @@ -96,7 +97,7 @@ class MediaCarouselScrollHandler( /** What's the currently visible player index? */ var visibleMediaIndex: Int = 0 - private set + @VisibleForTesting set /** How much are we scrolled into the current media? */ private var scrollIntoCurrentMedia: Int = 0 @@ -137,14 +138,14 @@ class MediaCarouselScrollHandler( eStart: MotionEvent?, eCurrent: MotionEvent, vX: Float, - vY: Float + vY: Float, ) = onFling(vX, vY) override fun onScroll( down: MotionEvent?, lastMotion: MotionEvent, distanceX: Float, - distanceY: Float + distanceY: Float, ) = onScroll(down!!, lastMotion, distanceX) override fun onDown(e: MotionEvent): Boolean { @@ -157,6 +158,7 @@ class MediaCarouselScrollHandler( val touchListener = object : Gefingerpoken { override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) + override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) } @@ -168,7 +170,7 @@ class MediaCarouselScrollHandler( scrollX: Int, scrollY: Int, oldScrollX: Int, - oldScrollY: Int + oldScrollY: Int, ) { if (playerWidthPlusPadding == 0) { return @@ -177,7 +179,7 @@ class MediaCarouselScrollHandler( val relativeScrollX = scrollView.relativeScrollX onMediaScrollingChanged( relativeScrollX / playerWidthPlusPadding, - relativeScrollX % playerWidthPlusPadding + relativeScrollX % playerWidthPlusPadding, ) } } @@ -209,7 +211,7 @@ class MediaCarouselScrollHandler( 0, carouselWidth, carouselHeight, - cornerRadius.toFloat() + cornerRadius.toFloat(), ) } } @@ -235,7 +237,7 @@ class MediaCarouselScrollHandler( getMaxTranslation().toFloat(), 0.0f, 1.0f, - Math.abs(contentTranslation) + Math.abs(contentTranslation), ) val settingsTranslation = (1.0f - settingsOffset) * @@ -323,7 +325,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig + config = translationConfig, ) .start() scrollView.animationTargetX = newTranslation @@ -391,7 +393,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig + config = translationConfig, ) .start() } else { @@ -430,7 +432,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = vX, - config = translationConfig + config = translationConfig, ) .start() scrollView.animationTargetX = newTranslation @@ -583,10 +585,35 @@ class MediaCarouselScrollHandler( // We need to post this to wait for the active player becomes visible. mainExecutor.executeDelayed( { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, - SCROLL_DELAY + SCROLL_DELAY, ) } + /** + * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond + * the carousel's bounds: + * - If the carousel is not dismissible, the settings button is displayed. + * - If the carousel is dismissible, no action taken. + * + * @param step A positive number means next, and negative means previous. + */ + fun scrollByStep(step: Int) { + val destIndex = visibleMediaIndex + step + if (destIndex >= mediaContent.childCount || destIndex < 0) { + if (!showsSettingsButton) return + var translation = getMaxTranslation() * sign(-step.toFloat()) + translation = if (isRtl) -translation else translation + PhysicsAnimator.getInstance(this) + .spring(CONTENT_TRANSLATION, translation, config = translationConfig) + .start() + scrollView.animationTargetX = translation + } else if (scrollView.getContentTranslation() != 0.0f) { + resetTranslation(true) + } else { + scrollToPlayer(destIndex = destIndex) + } + } + companion object { private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt index 16dff7d11002..11b014c2147f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt @@ -28,6 +28,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.qs.panels.shared.model.PanelsLog import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.settings.UserFileManager import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.SharedPreferencesExt.observe @@ -83,34 +84,78 @@ constructor( .flowOn(backgroundDispatcher) /** Sets for the current user the set of [TileSpec] to display as large tiles. */ - fun setLargeTilesSpecs(specs: Set<TileSpec>) { - setLargeTilesSpecsForUser(specs, userRepository.getSelectedUserInfo().id) + fun writeLargeTileSpecs(specs: Set<TileSpec>) { + with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) { + writeLargeTileSpecs(specs) + setLargeTilesDefault(false) + } } - private fun setLargeTilesSpecsForUser(specs: Set<TileSpec>, userId: Int) { - with(getSharedPrefs(userId)) { - edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply() + suspend fun deleteLargeTileDataJob() { + userRepository.selectedUserInfo.collect { userInfo -> + getSharedPrefs(userInfo.id) + .edit() + .remove(LARGE_TILES_SPECS_KEY) + .remove(LARGE_TILES_DEFAULT_KEY) + .apply() } } + private fun SharedPreferences.writeLargeTileSpecs(specs: Set<TileSpec>) { + edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply() + } + /** - * Sets the initial tiles as large, if there is no set in SharedPrefs for the [userId]. This is - * to be used when upgrading to a build that supports large/small tiles. + * Sets the initial set of large tiles. One of the following cases will happen: + * * If we are setting the default set (no value stored in settings for the list of tiles), set + * the large tiles based on [defaultLargeTilesRepository]. We do this to signal future reboots + * that we have performed the upgrade path once. In this case, we will mark that we set them + * as the default in case a restore needs to modify them later. + * * If we got a list of tiles restored from a device and nothing has modified the list of + * tiles, set all the restored tiles to large. Note that if we also restored a set of large + * tiles before this was called, [LARGE_TILES_DEFAULT_KEY] will be false and we won't + * overwrite it. + * * If we got a list of tiles from settings, we consider that we upgraded in place and then we + * will set all those tiles to large IF there's no current set of large tiles. * * Even if largeTilesSpec is read Eagerly before we know if we are in an initial state, because * we are not writing the default values to the SharedPreferences, the file will not contain the * key and this call will succeed, as long as there hasn't been any calls to setLargeTilesSpecs * for that user before. */ - fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, userId: Int) { + fun setInitialOrUpgradeLargeTiles(upgradePath: TilesUpgradePath, userId: Int) { with(getSharedPrefs(userId)) { - if (!contains(LARGE_TILES_SPECS_KEY)) { - logger.i("Setting upgraded large tiles for user $userId: $specs") - setLargeTilesSpecsForUser(specs, userId) + when (upgradePath) { + is TilesUpgradePath.DefaultSet -> { + writeLargeTileSpecs(defaultLargeTilesRepository.defaultLargeTiles) + logger.i("Large tiles set to default on init") + setLargeTilesDefault(true) + } + is TilesUpgradePath.RestoreFromBackup -> { + if ( + getBoolean(LARGE_TILES_DEFAULT_KEY, false) || + !contains(LARGE_TILES_SPECS_KEY) + ) { + writeLargeTileSpecs(upgradePath.value) + logger.i("Tiles restored from backup set to large: ${upgradePath.value}") + setLargeTilesDefault(false) + } + } + is TilesUpgradePath.ReadFromSettings -> { + if (!contains(LARGE_TILES_SPECS_KEY)) { + writeLargeTileSpecs(upgradePath.value) + logger.i("Tiles read from settings set to large: ${upgradePath.value}") + setLargeTilesDefault(false) + } + } } } } + private fun SharedPreferences.setLargeTilesDefault(value: Boolean) { + edit().putBoolean(LARGE_TILES_DEFAULT_KEY, value).apply() + } + private fun getSharedPrefs(userId: Int): SharedPreferences { return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId) } @@ -118,6 +163,7 @@ constructor( companion object { private const val TAG = "QSPreferencesRepository" private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs" + private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default" const val FILE_NAME = "quick_settings_prefs" } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt index 86838b438bc6..9b98797ef393 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -27,10 +28,20 @@ class QSPreferencesInteractor @Inject constructor(private val repo: QSPreference val largeTilesSpecs: Flow<Set<TileSpec>> = repo.largeTilesSpecs fun setLargeTilesSpecs(specs: Set<TileSpec>) { - repo.setLargeTilesSpecs(specs) + repo.writeLargeTileSpecs(specs) } - fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, user: Int) { - repo.setInitialLargeTilesSpecs(specs, user) + /** + * This method should be called to indicate that a "new" set of tiles has been determined for a + * particular user coming from different upgrade sources. + * + * @see TilesUpgradePath for more information + */ + fun setInitialOrUpgradeLargeTilesSpecs(specs: TilesUpgradePath, user: Int) { + repo.setInitialOrUpgradeLargeTiles(specs, user) + } + + suspend fun deleteLargeTilesDataJob() { + repo.deleteLargeTileDataJob() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt index a8ac5c34d8f9..e2797356fa96 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt @@ -19,11 +19,13 @@ package com.android.systemui.qs.panels.domain.startable import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.flags.QsInCompose import com.android.systemui.qs.panels.domain.interactor.QSPreferencesInteractor import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch class QSPanelsCoreStartable @Inject @@ -33,10 +35,14 @@ constructor( @Background private val backgroundApplicationScope: CoroutineScope, ) : CoreStartable { override fun start() { - backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") { - tileSpecRepository.tilesReadFromSetting.receiveAsFlow().collect { (tiles, userId) -> - preferenceInteractor.setInitialLargeTilesSpecs(tiles, userId) + if (QsInCompose.isEnabled) { + backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") { + tileSpecRepository.tilesUpgradePath.receiveAsFlow().collect { (tiles, userId) -> + preferenceInteractor.setInitialOrUpgradeLargeTilesSpecs(tiles, userId) + } } + } else { + backgroundApplicationScope.launch { preferenceInteractor.deleteLargeTilesDataJob() } } } } 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 6b7dd386bb46..c50d5dad10c1 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 @@ -24,6 +24,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.res.R import com.android.systemui.retail.data.repository.RetailModeRepository @@ -78,7 +79,7 @@ interface TileSpecRepository { /** Reset the current set of tiles to the default list of tiles */ suspend fun resetToDefault(userId: Int) - val tilesReadFromSetting: ReceiveChannel<Pair<Set<TileSpec>, Int>> + val tilesUpgradePath: ReceiveChannel<Pair<TilesUpgradePath, Int>> companion object { /** Position to indicate the end of the list */ @@ -112,8 +113,8 @@ constructor( .filter { it !is TileSpec.Invalid } } - private val _tilesReadFromSetting = Channel<Pair<Set<TileSpec>, Int>>(capacity = 5) - override val tilesReadFromSetting = _tilesReadFromSetting + private val _tilesUpgradePath = Channel<Pair<TilesUpgradePath, Int>>(capacity = 5) + override val tilesUpgradePath = _tilesUpgradePath private val userTileRepositories = SparseArray<UserTileSpecRepository>() @@ -122,8 +123,8 @@ constructor( val userTileRepository = userTileSpecRepositoryFactory.create(userId) userTileRepositories.put(userId, userTileRepository) applicationScope.launchTraced("TileSpecRepository.aggregateTilesPerUser") { - for (tilesFromSettings in userTileRepository.tilesReadFromSettings) { - _tilesReadFromSetting.send(tilesFromSettings to userId) + for (tileUpgrade in userTileRepository.tilesUpgradePath) { + _tilesUpgradePath.send(tileUpgrade to userId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt index 7b56cd92a081..5aa5edaa726e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt @@ -9,6 +9,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted @@ -49,8 +50,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) { - private val _tilesReadFromSettings = Channel<Set<TileSpec>>(capacity = 2) - val tilesReadFromSettings: ReceiveChannel<Set<TileSpec>> = _tilesReadFromSettings + private val _tilesUpgradePath = Channel<TilesUpgradePath>(capacity = 3) + val tilesUpgradePath: ReceiveChannel<TilesUpgradePath> = _tilesUpgradePath private val defaultTiles: List<TileSpec> get() = defaultTilesRepository.defaultTiles @@ -67,14 +68,23 @@ constructor( .scan(loadTilesFromSettingsAndParse(userId)) { current, change -> change .apply(current) - .also { - if (current != it) { + .also { afterRestore -> + if (current != afterRestore) { if (change is RestoreTiles) { - logger.logTilesRestoredAndReconciled(current, it, userId) + logger.logTilesRestoredAndReconciled( + current, + afterRestore, + userId, + ) } else { - logger.logProcessTileChange(change, it, userId) + logger.logProcessTileChange(change, afterRestore, userId) } } + if (change is RestoreTiles) { + _tilesUpgradePath.send( + TilesUpgradePath.RestoreFromBackup(afterRestore.toSet()) + ) + } } // Distinct preserves the order of the elements removing later // duplicates, @@ -154,7 +164,9 @@ constructor( private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> { val loadedTiles = loadTilesFromSettings(userId) if (loadedTiles.isNotEmpty()) { - _tilesReadFromSettings.send(loadedTiles.toSet()) + _tilesUpgradePath.send(TilesUpgradePath.ReadFromSettings(loadedTiles.toSet())) + } else { + _tilesUpgradePath.send(TilesUpgradePath.DefaultSet) } return parseTileSpecs(loadedTiles, userId) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt new file mode 100644 index 000000000000..98f30c22d0f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.shared + +/** Upgrade paths indicating the source of the list of QS tiles. */ +sealed interface TilesUpgradePath { + + sealed interface UpgradeWithTiles : TilesUpgradePath { + val value: Set<TileSpec> + } + + /** This indicates a set of tiles that was read from Settings on user start */ + @JvmInline value class ReadFromSettings(override val value: Set<TileSpec>) : UpgradeWithTiles + + /** This indicates a set of tiles that was restored from backup */ + @JvmInline value class RestoreFromBackup(override val value: Set<TileSpec>) : UpgradeWithTiles + + /** + * This indicates that no tiles were read from Settings on user start so the default has been + * stored. + */ + data object DefaultSet : TilesUpgradePath +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index a379ef7b0b96..305e71e48702 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -520,7 +520,10 @@ constructor( val glanceableHubV2 = communalSettingsInteractor.isV2FlagEnabled() if ( !hubShowing && - (touchOnNotifications || touchOnUmo || touchOnSmartspace || glanceableHubV2) + (touchOnNotifications || + touchOnUmo || + touchOnSmartspace || + !communalViewModel.swipeToHubEnabled()) ) { logger.d({ "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " + diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index 9a79e1a45505..ce48c85d57ae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -31,16 +31,26 @@ import android.os.Trace.TRACE_TAG_APP import android.provider.AlarmClock import android.view.DisplayCutout import android.view.View +import android.view.ViewGroup import android.view.WindowInsets import android.widget.TextView import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.view.doOnLayout +import androidx.core.view.isVisible +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.animation.Interpolators import com.android.settingslib.Utils import com.android.systemui.Dumpable import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.battery.BatteryMeterView +import com.android.systemui.battery.BatteryMeterView.MODE_ESTIMATE import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.demomode.DemoMode @@ -60,12 +70,15 @@ import com.android.systemui.shade.carrier.ShadeCarrierGroup import com.android.systemui.shade.carrier.ShadeCarrierGroupController import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager +import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryWithEstimate +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.NextAlarmController @@ -76,6 +89,7 @@ import dagger.Lazy import java.io.PrintWriter import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.flow.MutableStateFlow /** * Controller for QS header. @@ -100,6 +114,7 @@ constructor( private val shadeDisplaysRepositoryLazy: Lazy<ShadeDisplaysRepository>, private val variableDateViewControllerFactory: VariableDateViewController.Factory, @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController, + private val batteryViewModelFactory: BatteryViewModel.Factory, private val dumpManager: DumpManager, private val shadeCarrierGroupControllerBuilder: ShadeCarrierGroupController.Builder, private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager, @@ -162,6 +177,8 @@ constructor( private var lastInsets: WindowInsets? = null private var nextAlarmIntent: PendingIntent? = null + private val showBatteryEstimate = MutableStateFlow(false) + private var qsDisabled = false private var visible = false set(value) { @@ -323,10 +340,6 @@ constructor( override fun onInit() { variableDateViewControllerFactory.create(date as VariableDateView).init() - batteryMeterViewController.init() - - // battery settings same as in QS icons - batteryMeterViewController.ignoreTunerUpdates() val fgColor = Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary) @@ -336,11 +349,36 @@ constructor( iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS) iconManager.setTint(fgColor, bgColor) - batteryIcon.updateColors( - fgColor /* foreground */, - bgColor /* background */, - fgColor, /* single tone (current default) */ - ) + if (!NewStatusBarIcons.isEnabled) { + batteryMeterViewController.init() + + // battery settings same as in QS icons + batteryMeterViewController.ignoreTunerUpdates() + + batteryIcon.isVisible = true + batteryIcon.updateColors( + fgColor /* foreground */, + bgColor /* background */, + fgColor, /* single tone (current default) */ + ) + } else { + // Configure the compose battery view + val batteryComposeView = + ComposeView(mView.context).apply { + setContent { + val showBatteryEstimate by showBatteryEstimate.collectAsStateWithLifecycle() + BatteryWithEstimate( + modifier = Modifier.height(17.dp).wrapContentWidth(), + viewModelFactory = batteryViewModelFactory, + isDark = { true }, + showEstimate = showBatteryEstimate, + ) + } + } + mView.requireViewById<ViewGroup>(R.id.hover_system_icons_container).apply { + addView(batteryComposeView, -1) + } + } carrierIconSlots = listOf(header.context.getString(com.android.internal.R.string.status_bar_mobile)) @@ -474,7 +512,11 @@ constructor( private fun updateBatteryMode() { qsBatteryModeController.getBatteryMode(cutout, qsExpandedFraction)?.let { - batteryIcon.setPercentShowMode(it) + if (NewStatusBarIcons.isEnabled) { + showBatteryEstimate.value = it == MODE_ESTIMATE + } else { + batteryIcon.setPercentShowMode(it) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index b9df9f868dc3..7d4b0ed6304c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -45,13 +45,17 @@ import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.domain.interactor.ShadeModeInteractorImpl +import com.android.systemui.window.dagger.WindowRootViewBlurModule import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Provider /** Module for classes related to the notification shade. */ -@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class]) +@Module( + includes = + [StartShadeModule::class, ShadeViewProviderModule::class, WindowRootViewBlurModule::class] +) abstract class ShadeModule { companion object { @Provides diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 25ebc8c1ffa1..f06565f1b6d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -24,8 +24,10 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_ import static android.os.Flags.allowPrivateProfile; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_NULL; +import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_IMMEDIATELY; import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; +import static android.provider.Settings.Secure.REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI; import static com.android.systemui.DejankUtils.whitelistIpcs; @@ -44,6 +46,7 @@ import android.database.ContentObserver; import android.database.ExecutorContentObserver; import android.net.Uri; import android.os.Looper; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -118,6 +121,11 @@ public class NotificationLockscreenUserManagerImpl implements Settings.Secure.getUriFor(LOCK_SCREEN_SHOW_NOTIFICATIONS); private static final Uri SHOW_PRIVATE_LOCKSCREEN = Settings.Secure.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + private static final Uri REDACT_OTP_ON_WIFI = + Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI); + + private static final Uri REDACT_OTP_IMMEDIATELY = + Settings.Secure.getUriFor(REDACT_OTP_NOTIFICATION_IMMEDIATELY); private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS = TimeUnit.MINUTES.toMillis(10); @@ -307,6 +315,9 @@ public class NotificationLockscreenUserManagerImpl implements @VisibleForTesting protected final AtomicBoolean mConnectedToWifi = new AtomicBoolean(false); + protected final AtomicBoolean mRedactOtpOnWifi = new AtomicBoolean(true); + protected final AtomicBoolean mRedactOtpImmediately = new AtomicBoolean(false); + protected int mCurrentUserId = 0; protected NotificationPresenter mPresenter; @@ -363,6 +374,8 @@ public class NotificationLockscreenUserManagerImpl implements mLockScreenUris.add(SHOW_LOCKSCREEN); mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN); + mLockScreenUris.add(REDACT_OTP_ON_WIFI); + mLockScreenUris.add(REDACT_OTP_IMMEDIATELY); dumpManager.registerDumpable(this); @@ -432,6 +445,10 @@ public class NotificationLockscreenUserManagerImpl implements changed |= updateUserShowSettings(user.getIdentifier()); } else if (SHOW_PRIVATE_LOCKSCREEN.equals(uri)) { changed |= updateUserShowPrivateSettings(user.getIdentifier()); + } else if (REDACT_OTP_ON_WIFI.equals(uri)) { + changed |= updateRedactOtpOnWifiSetting(); + } else if (REDACT_OTP_IMMEDIATELY.equals(uri)) { + changed |= updateRedactOtpImmediatelySetting(); } } @@ -465,6 +482,14 @@ public class NotificationLockscreenUserManagerImpl implements true, mLockscreenSettingsObserver, USER_ALL); + mSecureSettings.registerContentObserverAsync( + REDACT_OTP_ON_WIFI, + mLockscreenSettingsObserver + ); + mSecureSettings.registerContentObserverAsync( + REDACT_OTP_IMMEDIATELY, + mLockscreenSettingsObserver + ); mBroadcastDispatcher.registerReceiver(mAllUsersReceiver, @@ -602,6 +627,28 @@ public class NotificationLockscreenUserManagerImpl implements } @WorkerThread + private boolean updateRedactOtpOnWifiSetting() { + boolean originalValue = mRedactOtpOnWifi.get(); + boolean newValue = mSecureSettings.getIntForUser( + REDACT_OTP_NOTIFICATION_WHILE_CONNECTED_TO_WIFI, + 0, + Process.myUserHandle().getIdentifier()) != 0; + mRedactOtpOnWifi.set(newValue); + return originalValue != newValue; + } + + @WorkerThread + private boolean updateRedactOtpImmediatelySetting() { + boolean originalValue = mRedactOtpImmediately.get(); + boolean newValue = mSecureSettings.getIntForUser( + REDACT_OTP_NOTIFICATION_IMMEDIATELY, + 0, + Process.myUserHandle().getIdentifier()) != 0; + mRedactOtpImmediately.set(newValue); + return originalValue != newValue; + } + + @WorkerThread private boolean updateGlobalKeyguardSettings() { final boolean oldValue = mKeyguardAllowingNotifications; mKeyguardAllowingNotifications = mKeyguardManager.getPrivateNotificationsAllowed(); @@ -769,23 +816,31 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - if (mConnectedToWifi.get()) { - return false; + if (!mRedactOtpOnWifi.get()) { + if (mConnectedToWifi.get()) { + return false; + } + + long lastWifiConnectTime = mLastWifiConnectionTime.get(); + // If the device has connected to wifi since receiving the notification, do not redact + if (ent.getSbn().getPostTime() < lastWifiConnectTime) { + return false; + } } if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) { return false; } - long lastWifiConnectTime = mLastWifiConnectionTime.get(); - // If the device has connected to wifi since receiving the notification, do not redact - if (ent.getSbn().getPostTime() < lastWifiConnectTime) { - return false; + long latestTimeForRedaction; + if (mRedactOtpImmediately.get()) { + latestTimeForRedaction = mLastLockTime.get(); + } else { + // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS + // when this notification arrived, do not redact + latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS; } - // If the lock screen was not already locked for LOCK_TIME_FOR_SENSITIVE_REDACTION_MS when - // this notification arrived, do not redact - long latestTimeForRedaction = mLastLockTime.get() + LOCK_TIME_FOR_SENSITIVE_REDACTION_MS; if (ent.getSbn().getPostTime() < latestTimeForRedaction) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index a2c0226addfa..f466278e15a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -32,9 +32,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor -import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel -import com.android.systemui.statusbar.chips.ui.model.ColorsModel.Companion.toCustomColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel @@ -86,12 +84,7 @@ constructor( OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) } - val colors = - if (StatusBarNotifChips.isEnabled && state.promotedContent != null) { - state.promotedContent.toCustomColorsModel() - } else { - ColorsModel.Themed - } + val colors = ColorsModel.AccentThemed // This block mimics OngoingCallController#updateChip. if (state.startTimeMs <= 0L) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 8357df42937e..2d6102e310f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -27,7 +27,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.chips.ui.model.ColorsModel.Companion.toCustomColorsModel +import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor @@ -85,8 +85,7 @@ constructor( contentDescription, ) } - val colors = this.promotedContent.toCustomColorsModel() - + val colors = ColorsModel.SystemThemed val clickListener: () -> Unit = { // The notification pipeline needs everything to run on the main thread, so keep // this event on the main thread. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 456cd121a540..d41353b2c176 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.ui.binder import android.annotation.IdRes +import android.content.Context import android.content.res.ColorStateList import android.graphics.drawable.GradientDrawable import android.view.View @@ -32,6 +33,7 @@ import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.view.ChipChronometer @@ -76,8 +78,10 @@ object OngoingActivityChipBinder { chipTimeView.setTextColor(textColor) chipTextView.setTextColor(textColor) chipShortTimeDeltaView.setTextColor(textColor) - (chipBackgroundView.background as GradientDrawable).color = - chipModel.colors.background(chipContext) + (chipBackgroundView.background as GradientDrawable).setBackgroundColors( + chipModel.colors, + chipContext, + ) } is OngoingActivityChipModel.Inactive -> { // The Chronometer should be stopped to prevent leaks -- see b/192243808 and @@ -460,5 +464,20 @@ object OngoingActivityChipBinder { chipView.minimumWidth = minimumWidth } + private fun GradientDrawable.setBackgroundColors(colors: ColorsModel, context: Context) { + this.color = colors.background(context) + val outline = colors.outline(context) + if (outline != null) { + this.setStroke( + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_outline_width + ), + outline, + ) + } else { + this.setStroke(0, /* color= */ 0) + } + } + @IdRes private val CUSTOM_ICON_VIEW_ID = R.id.ongoing_activity_chip_custom_icon } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index 32de0fbfd870..8443d106dfb1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -20,16 +20,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope @@ -37,6 +30,8 @@ import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp @@ -83,15 +78,14 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = softWrap = false, modifier = modifier - .customTextContentLayout( + .hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, maxTextWidth = maxTextWidth, startPadding = startPadding, endPadding = endPadding, - ) { constraintWidth -> - val intrinsicWidth = - textMeasurer.measure(text, textStyle, softWrap = false).size.width - intrinsicWidth <= constraintWidth - } + ) .neverDecreaseWidth(), ) } @@ -108,7 +102,6 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = } is OngoingActivityChipModel.Active.Text -> { - var hasOverflow by remember { mutableStateOf(false) } val text = viewModel.text Text( text = text, @@ -116,24 +109,14 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = style = textStyle, softWrap = false, modifier = - modifier - .customTextContentLayout( - maxTextWidth = maxTextWidth, - startPadding = startPadding, - endPadding = endPadding, - ) { constraintWidth -> - val intrinsicWidth = - textMeasurer.measure(text, textStyle, softWrap = false).size.width - hasOverflow = intrinsicWidth > constraintWidth - constraintWidth.toFloat() / intrinsicWidth.toFloat() > 0.5f - } - .overflowFadeOut( - hasOverflow = { hasOverflow }, - fadeLength = - dimensionResource( - id = R.dimen.ongoing_activity_chip_text_fading_edge_length - ), - ), + modifier.hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ), ) } @@ -180,45 +163,67 @@ private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode { } /** - * A custom layout modifier for text that ensures its text is only visible if a provided - * [shouldShow] callback returns true. Imposes a provided [maxTextWidthPx]. Also, accounts for - * provided padding values if provided and ensures its text is placed with the provided padding - * included around it. + * A custom layout modifier for text that ensures the text is only visible if it completely fits + * within the constrained bounds. Imposes a provided [maxTextWidthPx]. Also, accounts for provided + * padding values if provided and ensures its text is placed with the provided padding included + * around it. */ -private fun Modifier.customTextContentLayout( +private fun Modifier.hideTextIfDoesNotFit( + text: String, + textStyle: TextStyle, + textMeasurer: TextMeasurer, maxTextWidth: Dp, startPadding: Dp = 0.dp, endPadding: Dp = 0.dp, - shouldShow: (constraintWidth: Int) -> Boolean, ): Modifier { return this.then( - CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow) + HideTextIfDoesNotFitElement( + text, + textStyle, + textMeasurer, + maxTextWidth, + startPadding, + endPadding, + ) ) } -private data class CustomTextContentLayoutElement( +private data class HideTextIfDoesNotFitElement( + val text: String, + val textStyle: TextStyle, + val textMeasurer: TextMeasurer, val maxTextWidth: Dp, val startPadding: Dp, val endPadding: Dp, - val shouldShow: (constrainedWidth: Int) -> Boolean, -) : ModifierNodeElement<CustomTextContentLayoutNode>() { - override fun create(): CustomTextContentLayoutNode { - return CustomTextContentLayoutNode(maxTextWidth, startPadding, endPadding, shouldShow) +) : ModifierNodeElement<HideTextIfDoesNotFitNode>() { + override fun create(): HideTextIfDoesNotFitNode { + return HideTextIfDoesNotFitNode( + text, + textStyle, + textMeasurer, + maxTextWidth, + startPadding, + endPadding, + ) } - override fun update(node: CustomTextContentLayoutNode) { - node.shouldShow = shouldShow + override fun update(node: HideTextIfDoesNotFitNode) { + node.text = text + node.textStyle = textStyle + node.textMeasurer = textMeasurer node.maxTextWidth = maxTextWidth node.startPadding = startPadding node.endPadding = endPadding } } -private class CustomTextContentLayoutNode( +private class HideTextIfDoesNotFitNode( + var text: String, + var textStyle: TextStyle, + var textMeasurer: TextMeasurer, var maxTextWidth: Dp, var startPadding: Dp, var endPadding: Dp, - var shouldShow: (constrainedWidth: Int) -> Boolean, ) : Modifier.Node(), LayoutModifierNode { override fun MeasureScope.measure( measurable: Measurable, @@ -230,9 +235,10 @@ private class CustomTextContentLayoutNode( .coerceAtLeast(constraints.minWidth) val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth)) - val height = placeable.height - val width = placeable.width - return if (shouldShow(maxWidth)) { + val intrinsicWidth = textMeasurer.measure(text, textStyle, softWrap = false).size.width + return if (intrinsicWidth <= maxWidth) { + val height = placeable.height + val width = placeable.width layout(width + horizontalPadding.roundToPx(), height) { placeable.place(startPadding.roundToPx(), 0) } @@ -241,20 +247,3 @@ private class CustomTextContentLayoutNode( } } } - -private fun Modifier.overflowFadeOut(hasOverflow: () -> Boolean, fadeLength: Dp): Modifier { - return graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithCache { - val width = size.width - val start = (width - fadeLength.toPx()).coerceAtLeast(0f) - val gradient = - Brush.horizontalGradient( - colors = listOf(Color.Black, Color.Transparent), - startX = start, - endX = width, - ) - onDrawWithContent { - drawContent() - if (hasOverflow()) drawRect(brush = gradient, blendMode = BlendMode.DstIn) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 76c53861f0ab..1cdf6800fb97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.ui.compose import android.content.res.ColorStateList import android.view.ViewGroup import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -103,6 +104,13 @@ private fun ChipBody( } else { dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding } + + val outline = model.colors.outline(context) + val outlineWidth = dimensionResource(R.dimen.ongoing_activity_chip_outline_width) + + val shape = + RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)) + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible // height of the chip is determined by the height of the background of the Row below. Box( @@ -121,12 +129,7 @@ private fun ChipBody( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = - Modifier.clip( - RoundedCornerShape( - dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) - ) - ) - .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) + Modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)) .thenIf(isClickable) { Modifier.widthIn(min = minWidth) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) @@ -136,7 +139,14 @@ private fun ChipBody( } } } - .background(Color(model.colors.background(context).defaultColor)) + .background(Color(model.colors.background(context).defaultColor), shape = shape) + .thenIf(outline != null) { + Modifier.border( + width = outlineWidth, + color = Color(outline!!), + shape = shape, + ) + } .padding( horizontal = if (hasEmbeddedIcon) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index 25f90f9a0065..4954cb0a1b24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -21,7 +21,6 @@ import android.content.res.ColorStateList import androidx.annotation.ColorInt import com.android.settingslib.Utils import com.android.systemui.res.R -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel /** Model representing how the chip in the status bar should be colored. */ sealed interface ColorsModel { @@ -31,13 +30,38 @@ sealed interface ColorsModel { /** The color for the text (and icon) on the chip. */ @ColorInt fun text(context: Context): Int - /** The chip should match the theme's primary color. */ - data object Themed : ColorsModel { + /** The color to use for the chip outline, or null if the chip shouldn't have an outline. */ + @ColorInt fun outline(context: Context): Int? + + /** The chip should match the theme's primary accent color. */ + // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it + // only gets updated when a different configuration change happens, like a rotation. + data object AccentThemed : ColorsModel { override fun background(context: Context): ColorStateList = Utils.getColorAttr(context, com.android.internal.R.attr.colorAccent) override fun text(context: Context) = Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) + + override fun outline(context: Context) = null + } + + /** The chip should match the system theme main color. */ + // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it + // only gets updated when a different configuration change happens, like a rotation. + data object SystemThemed : ColorsModel { + override fun background(context: Context): ColorStateList = + ColorStateList.valueOf( + context.getColor(com.android.internal.R.color.materialColorSurfaceDim) + ) + + override fun text(context: Context) = + context.getColor(com.android.internal.R.color.materialColorOnSurface) + + override fun outline(context: Context) = + // Outline is required on the SystemThemed chip to guarantee the chip doesn't completely + // blend in with the background. + context.getColor(com.android.internal.R.color.materialColorOutlineVariant) } /** The chip should have the given background color and primary text color. */ @@ -46,6 +70,8 @@ sealed interface ColorsModel { ColorStateList.valueOf(backgroundColorInt) override fun text(context: Context): Int = primaryTextColorInt + + override fun outline(context: Context) = null } /** The chip should have a red background with white text. */ @@ -55,15 +81,7 @@ sealed interface ColorsModel { } override fun text(context: Context) = context.getColor(android.R.color.white) - } - companion object { - /** Converts the promoted notification colors to a [Custom] colors model. */ - fun PromotedNotificationContentModel.toCustomColorsModel(): Custom { - return Custom( - backgroundColorInt = this.colors.backgroundColor, - primaryTextColorInt = this.colors.primaryTextColor, - ) - } + override fun outline(context: Context) = null } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt index 52495eb55436..c19b144b7f42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipTextTruncationHelper.kt @@ -51,9 +51,8 @@ class ChipTextTruncationHelper(private val view: View) { } /** - * Returns true if this view should show the text because there's enough room for a substantial - * amount of text, and returns false if this view should hide the text because the text is much - * too long. + * Returns true if this view should show the text because there's enough room for all the text, + * and returns false if this view should hide the text because not all of it fits. * * @param desiredTextWidthPx should be calculated by having the view measure itself with * [unlimitedWidthMeasureSpec] and then sending its `measuredWidth` to this method. (This @@ -82,9 +81,8 @@ class ChipTextTruncationHelper(private val view: View) { enforcedTextWidth = maxWidthBasedOnDimension } - // Only show the text if at least 50% of it can show. (Assume that if < 50% of the text will - // be visible, the text will be more confusing than helpful.) - return desiredTextWidthPx <= enforcedTextWidth * 2 + // Only show the text if all of it can show + return desiredTextWidthPx <= enforcedTextWidth } private fun fetchMaxWidth() = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 3ba0ae3b3cb6..1a30caf0150b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -214,7 +214,6 @@ constructor( if ( secondaryChip is InternalChipModel.Active && StatusBarNotifChips.isEnabled && - !StatusBarChipsModernization.isEnabled && !isScreenReasonablyLarge ) { // If we have two showing chips and we don't have a ton of room @@ -222,8 +221,10 @@ constructor( // possible so that we have the highest chance of showing both chips (as // opposed to showing the primary chip with a lot of text and completely // hiding the secondary chip). - // Also: If StatusBarChipsModernization is enabled, then we'll do the - // squishing in Compose instead. + // TODO(b/392895330): If StatusBarChipsModernization is enabled, do the + // squishing in Compose instead, and be smart about it (e.g. if we have + // room for the first chip to show text and the second chip to be icon-only, + // do that instead of always squishing both chips.) InternalMultipleOngoingActivityChipsModel( primaryChip.squish(), secondaryChip.squish(), @@ -237,24 +238,31 @@ constructor( /** Squishes the chip down to the smallest content possible. */ private fun InternalChipModel.Active.squish(): InternalChipModel.Active { - return when (model) { + return if (model.shouldSquish()) { + InternalChipModel.Active(this.type, this.model.toIconOnly()) + } else { + this + } + } + + private fun OngoingActivityChipModel.Active.shouldSquish(): Boolean { + return when (this) { // Icon-only is already maximum squished - is OngoingActivityChipModel.Active.IconOnly -> this + is OngoingActivityChipModel.Active.IconOnly, // Countdown shows just a single digit, so already maximum squished - is OngoingActivityChipModel.Active.Countdown -> this - // The other chips have icon+text, so we should hide the text + is OngoingActivityChipModel.Active.Countdown -> false + // The other chips have icon+text, so we can squish them by hiding text is OngoingActivityChipModel.Active.Timer, is OngoingActivityChipModel.Active.ShortTimeDelta, - is OngoingActivityChipModel.Active.Text -> - InternalChipModel.Active(this.type, this.model.toIconOnly()) + is OngoingActivityChipModel.Active.Text -> true } } private fun OngoingActivityChipModel.Active.toIconOnly(): OngoingActivityChipModel.Active { // If this chip doesn't have an icon, then it only has text and we should continue showing // its text. (This is theoretically impossible because - // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon, but protect - // against it just in case.) + // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and + // [shouldSquish] returns false for that model, but protect against it just in case.) val currentIcon = icon ?: return this return OngoingActivityChipModel.Active.IconOnly( key, @@ -271,8 +279,38 @@ constructor( */ val chips: StateFlow<MultipleOngoingActivityChipsModel> = if (StatusBarChipsModernization.isEnabled) { - incomingChipBundle - .map { bundle -> rankChips(bundle) } + combine( + incomingChipBundle.map { bundle -> rankChips(bundle) }, + isScreenReasonablyLarge, + ) { rankedChips, isScreenReasonablyLarge -> + if ( + StatusBarNotifChips.isEnabled && + !isScreenReasonablyLarge && + rankedChips.active.filter { !it.isHidden }.size >= 2 + ) { + // If we have at least two showing chips and we don't have a ton of room + // (!isScreenReasonablyLarge), then we want to make both of them as small as + // possible so that we have the highest chance of showing both chips (as + // opposed to showing the first chip with a lot of text and completely + // hiding the other chips). + val squishedActiveChips = + rankedChips.active.map { + if (!it.isHidden && it.shouldSquish()) { + it.toIconOnly() + } else { + it + } + } + + MultipleOngoingActivityChipsModel( + active = squishedActiveChips, + overflow = rankedChips.overflow, + inactive = rankedChips.inactive, + ) + } else { + rankedChips + } + } .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel()) } else { MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt index 6ceeb6aae7a5..bcaf1878a869 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt @@ -26,7 +26,7 @@ import dagger.Module * This is meant to be bound in SystemUI variants with [NotificationStackScrollLayoutController]. */ @Module -interface NotificationStackGoogleModule { +interface NotificationStackModule { @Binds fun bindNotificationStackRebindingHider( impl: NotificationStackRebindingHiderImpl @@ -35,7 +35,7 @@ interface NotificationStackGoogleModule { /** This is meant to be used by all SystemUI variants, also those without NSSL. */ @Module -interface NotificationStackModule { +interface NotificationStackOptionalModule { @BindsOptionalOf fun bindOptionalOfNotificationStackRebindingHider(): NotificationStackRebindingHider } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index e10825bc52fe..34f4969127e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -121,7 +121,7 @@ import javax.inject.Provider; NotificationMemoryModule.class, NotificationStatsLoggerModule.class, NotificationsLogModule.class, - NotificationStackModule.class, + NotificationStackOptionalModule.class, }) public interface NotificationsModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt index de113d365bd8..ccc2dffcfd7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt @@ -49,7 +49,7 @@ constructor( ) : Dumpable { private val tag = "AvalancheController" - private val debug = Compile.IS_DEBUG + private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) var baseEntryMapStr: () -> String = { "baseEntryMapStr not initialized" } var enableAtRuntime = true diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index 7c75983885ea..777ffda8c87d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -231,6 +231,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { ) { // Icon binding must be called in this order updateImageView(icon, content.smallIcon) + icon?.setImageLevel(content.iconLevel) icon?.setBackgroundColor(Background.colorInt) icon?.originalIconColor = PrimaryText.colorInt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index cd7872291801..39c7df064c8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -96,6 +96,7 @@ constructor( contentBuilder.wasPromotedAutomatically = notification.extras.getBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) contentBuilder.smallIcon = notification.smallIconModel(imageModelProvider) + contentBuilder.iconLevel = notification.iconLevel contentBuilder.appName = notification.loadHeaderAppName(context) contentBuilder.subText = notification.subText() contentBuilder.time = notification.extractWhen() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index af5a8203c979..38d41e37f916 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -38,6 +38,7 @@ data class PromotedNotificationContentModel( */ val wasPromotedAutomatically: Boolean, val smallIcon: ImageModel?, + val iconLevel: Int, val appName: CharSequence?, val subText: CharSequence?, val shortCriticalText: String?, @@ -67,6 +68,7 @@ data class PromotedNotificationContentModel( class Builder(val key: String) { var wasPromotedAutomatically: Boolean = false var smallIcon: ImageModel? = null + var iconLevel: Int = 0 var appName: CharSequence? = null var subText: CharSequence? = null var time: When? = null @@ -94,6 +96,7 @@ data class PromotedNotificationContentModel( identity = Identity(key, style), wasPromotedAutomatically = wasPromotedAutomatically, smallIcon = smallIcon, + iconLevel = iconLevel, appName = appName, subText = subText, shortCriticalText = shortCriticalText, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 58326dbb3a34..fa4fe46e690c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -47,6 +47,7 @@ import com.android.settingslib.Utils; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.res.R; +import com.android.systemui.statusbar.core.NewStatusBarIcons; import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider; import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange; import com.android.systemui.statusbar.phone.ui.TintedIconManager; @@ -79,7 +80,7 @@ public class KeyguardStatusBarView extends RelativeLayout { private TextView mCarrierLabel; private ImageView mMultiUserAvatar; - private BatteryMeterView mBatteryView; + @Nullable private BatteryMeterView mBatteryView; private StatusIconContainer mStatusIconContainer; private StatusBarUserSwitcherContainer mUserSwitcherContainer; @@ -131,6 +132,11 @@ public class KeyguardStatusBarView extends RelativeLayout { mMultiUserAvatar = findViewById(R.id.multi_user_avatar); mCarrierLabel = findViewById(R.id.keyguard_carrier_text); mBatteryView = mSystemIconsContainer.findViewById(R.id.battery); + if (NewStatusBarIcons.isEnabled()) { + // When this flag is rolled forward, this whole view can be removed + mBatteryView.setVisibility(View.GONE); + mBatteryView = null; + } mCutoutSpace = findViewById(R.id.cutout_space_view); mStatusIconArea = findViewById(R.id.status_icon_area); mStatusIconContainer = findViewById(R.id.statusIcons); @@ -259,7 +265,10 @@ public class KeyguardStatusBarView extends RelativeLayout { mMultiUserAvatar.setVisibility(View.GONE); } } - mBatteryView.setForceShowPercent(mBatteryCharging && mShowPercentAvailable); + + if (mBatteryView != null) { + mBatteryView.setForceShowPercent(mBatteryCharging && mShowPercentAvailable); + } } private void updateSystemIconsLayoutParams() { @@ -442,7 +451,9 @@ public class KeyguardStatusBarView extends RelativeLayout { /** Should only be called from {@link KeyguardStatusBarViewController}. */ void onThemeChanged(TintedIconManager iconManager) { - mBatteryView.setColorsFromContext(mContext); + if (mBatteryView != null) { + mBatteryView.setColorsFromContext(mContext); + } updateIconsAndTextColors(iconManager); } @@ -450,7 +461,9 @@ public class KeyguardStatusBarView extends RelativeLayout { void onOverlayChanged() { final int carrierTheme = R.style.TextAppearance_StatusBar_Clock; mCarrierLabel.setTextAppearance(carrierTheme); - mBatteryView.updatePercentView(); + if (mBatteryView != null) { + mBatteryView.updatePercentView(); + } final int userSwitcherTheme = R.style.TextAppearance_StatusBar_UserChip; TextView userSwitcherName = mUserSwitcherContainer.findViewById(R.id.current_user_name); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 40245aef4f67..de7215461c80 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -34,10 +34,12 @@ import android.provider.Settings; import android.util.MathUtils; import android.view.DisplayCutout; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.compose.ui.platform.ComposeView; import androidx.core.animation.Animator; import androidx.core.animation.AnimatorListenerAdapter; import androidx.core.animation.ValueAnimator; @@ -62,6 +64,7 @@ import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.core.NewStatusBarIcons; import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore; import com.android.systemui.statusbar.disableflags.DisableStateTracker; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; @@ -71,10 +74,13 @@ 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.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor; import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt; import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventDefaultAnimator; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.phone.ui.TintedIconManager; +import com.android.systemui.statusbar.pipeline.battery.ui.binder.UnifiedBatteryViewBinder; +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -125,6 +131,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final StatusBarIconController mStatusBarIconController; private final TintedIconManager.Factory mTintedIconManagerFactory; private final BatteryMeterViewController mBatteryMeterViewController; + private final BatteryViewModel.Factory mBatteryViewModelFactory; private final ShadeViewStateProvider mShadeViewStateProvider; private final KeyguardStateController mKeyguardStateController; private final KeyguardBypassController mKeyguardBypassController; @@ -145,7 +152,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final GlanceableHubToLockscreenTransitionViewModel mHubToLockscreenTransitionViewModel; private final LockscreenToGlanceableHubTransitionViewModel mLockscreenToHubTransitionViewModel; - private View mSystemIconsContainer; + private ViewGroup mSystemIconsContainer; private final StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory; private final ConfigurationController.ConfigurationListener mConfigurationListener = @@ -327,6 +334,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat StatusBarIconController statusBarIconController, TintedIconManager.Factory tintedIconManagerFactory, BatteryMeterViewController batteryMeterViewController, + BatteryViewModel.Factory batteryViewModelFactory, ShadeViewStateProvider shadeViewStateProvider, KeyguardStateController keyguardStateController, KeyguardBypassController bypassController, @@ -360,6 +368,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mStatusBarIconController = statusBarIconController; mTintedIconManagerFactory = tintedIconManagerFactory; mBatteryMeterViewController = batteryMeterViewController; + mBatteryViewModelFactory = batteryViewModelFactory; mShadeViewStateProvider = shadeViewStateProvider; mKeyguardStateController = keyguardStateController; mKeyguardBypassController = bypassController; @@ -417,7 +426,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat protected void onInit() { super.onInit(); mCarrierTextController.init(); - mBatteryMeterViewController.init(); + if (!NewStatusBarIcons.isEnabled()) { + mBatteryMeterViewController.init(); + } if (isMigrationEnabled()) { KeyguardStatusBarViewBinder.bind(mView, mKeyguardStatusBarViewModel); } @@ -469,6 +480,15 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mToGlanceableHubStatusBarAlphaConsumer, mCoroutineDispatcher); collectFlow(mView, mHubToLockscreenTransitionViewModel.getStatusBarAlpha(), mFromGlanceableHubStatusBarAlphaConsumer, mCoroutineDispatcher); + if (NewStatusBarIcons.isEnabled()) { + ComposeView batteryComposeView = new ComposeView(mContext); + UnifiedBatteryViewBinder.bind( + batteryComposeView, + mBatteryViewModelFactory, + DarkIconInteractor.toIsAreaDark(mView.darkChangeFlow())); + + mSystemIconsContainer.addView(batteryComposeView, -1); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 4d222fdb90ea..b2d337797b53 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -81,6 +81,9 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.wakelock.DelayedWakeLock; import com.android.systemui.util.wakelock.WakeLock; +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor; + +import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; @@ -226,7 +229,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private float mScrimBehindAlphaKeyguard = KEYGUARD_SCRIM_ALPHA; static final float TRANSPARENT_BOUNCER_SCRIM_ALPHA = 0.54f; - private final float mDefaultScrimAlpha; + private float mDefaultScrimAlpha; private float mRawPanelExpansionFraction; private float mPanelScrimMinFraction; @@ -257,6 +260,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener; private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; private final BlurConfig mBlurConfig; + private final Lazy<WindowRootViewBlurInteractor> mWindowRootViewBlurInteractor; private Consumer<Integer> mScrimVisibleListener; private boolean mBlankScreen; private boolean mScreenBlankingCallbackCalled; @@ -339,14 +343,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump KeyguardInteractor keyguardInteractor, @Main CoroutineDispatcher mainDispatcher, LargeScreenShadeInterpolator largeScreenShadeInterpolator, - BlurConfig blurConfig) { + BlurConfig blurConfig, + Lazy<WindowRootViewBlurInteractor> windowRootViewBlurInteractor) { mScrimStateListener = lightBarController::setScrimState; mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; mBlurConfig = blurConfig; - // All scrims default alpha need to match bouncer background alpha to make sure the - // transitions involving the bouncer are smooth and don't overshoot the bouncer alpha. - mDefaultScrimAlpha = - Flags.bouncerUiRevamp() ? TRANSPARENT_BOUNCER_SCRIM_ALPHA : BUSY_SCRIM_ALPHA; + mWindowRootViewBlurInteractor = windowRootViewBlurInteractor; + mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; mKeyguardStateController = keyguardStateController; mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); @@ -407,7 +410,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final ScrimState[] states = ScrimState.values(); for (int i = 0; i < states.length; i++) { - states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager, mBlurConfig); + states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager); states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard); states[i].setDefaultScrimAlpha(mDefaultScrimAlpha); } @@ -485,6 +488,30 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump Edge.Companion.create(Scenes.Communal, LOCKSCREEN), Edge.Companion.create(GLANCEABLE_HUB, LOCKSCREEN)), mGlanceableHubConsumer, mMainDispatcher); + + if (Flags.bouncerUiRevamp()) { + collectFlow(behindScrim, + mWindowRootViewBlurInteractor.get().isBlurCurrentlySupported(), + this::handleBlurSupportedChanged); + } + } + + private void updateDefaultScrimAlpha(float alpha) { + mDefaultScrimAlpha = alpha; + for (ScrimState state : ScrimState.values()) { + state.setDefaultScrimAlpha(mDefaultScrimAlpha); + } + applyAndDispatchState(); + } + + private void handleBlurSupportedChanged(boolean isBlurSupported) { + if (isBlurSupported) { + updateDefaultScrimAlpha(TRANSPARENT_BOUNCER_SCRIM_ALPHA); + ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx()); + } else { + ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(0f); + updateDefaultScrimAlpha(BUSY_SCRIM_ALPHA); + } } // TODO(b/270984686) recompute scrim height accurately, based on shade contents. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 5f423cf35edd..071a57a8b298 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -17,14 +17,12 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.phone.ScrimController.BUSY_SCRIM_ALPHA; -import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT_BOUNCER_SCRIM_ALPHA; import android.graphics.Color; import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.Flags; import com.android.systemui.dock.DockManager; -import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.ui.ShadeColors; @@ -116,8 +114,8 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { if (Flags.bouncerUiRevamp()) { - mBehindAlpha = mClipQsScrim ? 0.0f : TRANSPARENT_BOUNCER_SCRIM_ALPHA; - mNotifAlpha = mClipQsScrim ? TRANSPARENT_BOUNCER_SCRIM_ALPHA : 0; + mBehindAlpha = mDefaultScrimAlpha; + mNotifAlpha = 0f; mBehindTint = mNotifTint = mSurfaceColor; mFrontAlpha = 0f; return; @@ -153,12 +151,11 @@ public enum ScrimState { if (previousState == SHADE_LOCKED) { mBehindAlpha = previousState.getBehindAlpha(); mNotifAlpha = previousState.getNotifAlpha(); - mNotifBlurRadius = mBlurConfig.getMaxBlurRadiusPx(); } else { mNotifAlpha = 0f; mBehindAlpha = 0f; } - mFrontAlpha = TRANSPARENT_BOUNCER_SCRIM_ALPHA; + mFrontAlpha = mDefaultScrimAlpha; mFrontTint = mSurfaceColor; return; } @@ -403,7 +400,6 @@ public enum ScrimState { DozeParameters mDozeParameters; DockManager mDockManager; boolean mDisplayRequiresBlanking; - protected BlurConfig mBlurConfig; boolean mLaunchingAffordanceWithPreview; boolean mOccludeAnimationPlaying; boolean mWakeLockScreenSensorActive; @@ -417,7 +413,7 @@ public enum ScrimState { protected float mNotifBlurRadius = 0.0f; public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters, - DockManager dockManager, BlurConfig blurConfig) { + DockManager dockManager) { mBackgroundColor = scrimBehind.getContext().getColor(R.color.shade_scrim_background_dark); mScrimInFront = scrimInFront; mScrimBehind = scrimBehind; @@ -425,7 +421,6 @@ public enum ScrimState { mDozeParameters = dozeParameters; mDockManager = dockManager; mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking(); - mBlurConfig = blurConfig; } /** Prepare state for transition. */ @@ -536,4 +531,8 @@ public enum ScrimState { public float getNotifBlurRadius() { return mNotifBlurRadius; } + + public void setNotifBlurRadius(float value) { + mNotifBlurRadius = value; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index e33baf7c33ae..ded964d8a1cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -57,11 +57,11 @@ import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.statusbar.CommandQueue; @@ -76,11 +76,11 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; -import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.wmshell.BubblesManager; @@ -115,7 +115,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final static String TAG = "StatusBarNotificationActivityStarter"; private final Context mContext; - private final int mDisplayId; private final Handler mMainThreadHandler; private final Executor mUiBgExecutor; @@ -155,8 +154,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Inject StatusBarNotificationActivityStarter( - Context context, - @DisplayId int displayId, + @ShadeDisplayAware Context context, Handler mainThreadHandler, @Background Executor uiBgExecutor, NotificationVisibilityProvider visibilityProvider, @@ -189,7 +187,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit PowerInteractor powerInteractor, UserTracker userTracker) { mContext = context; - mDisplayId = displayId; mMainThreadHandler = mainThreadHandler; mUiBgExecutor = uiBgExecutor; mVisibilityProvider = visibilityProvider; @@ -493,6 +490,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean animate, boolean isActivityIntent) { mLogger.logStartNotificationIntent(entry); + final int displayId = mContext.getDisplayId(); try { ActivityTransitionAnimator.Controller animationController = new StatusBarTransitionAnimatorController( @@ -501,7 +499,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, isActivityIntent); mActivityTransitionAnimator.startPendingIntentWithAnimation( animationController, @@ -511,11 +509,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit long eventTime = row.getAndResetLastActionUpTime(); Bundle options = eventTime > 0 ? getActivityOptions( - mDisplayId, + displayId, adapter, mKeyguardStateController.isShowing(), eventTime) - : getActivityOptions(mDisplayId, adapter); + : getActivityOptions(displayId, adapter); int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null, null, null, options); mLogger.logSendPendingIntent(entry, intent, result); @@ -533,6 +531,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid, @NonNull ExpandableNotificationRow row) { boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); + final int displayId = mContext.getDisplayId(); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { @@ -544,7 +543,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( @@ -552,7 +551,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit (adapter) -> TaskStackBuilder.create(mContext) .addNextIntentWithParentStack(intent) .startActivities(getActivityOptions( - mDisplayId, + displayId, adapter), new UserHandle(UserHandle.getUserId(appUid)))); }); @@ -571,6 +570,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startHistoryIntent(View view, boolean showHistory) { ModesEmptyShadeFix.assertInLegacyMode(); + final int displayId = mContext.getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -597,13 +597,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intent.getPackage(), (adapter) -> tsb.startActivities( - getActivityOptions(mDisplayId, adapter), + getActivityOptions(displayId, adapter), mUserTracker.getUserHandle())); }); return true; @@ -620,6 +620,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) { + final int displayId = mContext.getDisplayId(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -642,13 +643,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mShadeController, mNotificationShadeWindowController, mCommandQueue, - mDisplayId, + displayId, true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intentInfo.getTargetIntent().getPackage(), (adapter) -> tsb.startActivities( - getActivityOptions(mDisplayId, adapter), + getActivityOptions(displayId, adapter), mUserTracker.getUserHandle())); }); return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java index 7207d0aef3ee..4d531b512dd9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java @@ -20,6 +20,7 @@ import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.dagger.qualifiers.DisplaySpecific; import com.android.systemui.dagger.qualifiers.RootView; import com.android.systemui.plugins.DarkIconDispatcher; +import com.android.systemui.statusbar.core.NewStatusBarIcons; import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController; import com.android.systemui.statusbar.layout.StatusBarBoundsProvider; import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; @@ -85,7 +86,9 @@ public interface HomeStatusBarComponent { default void init() { // No one accesses these controllers, so we need to make sure we reference them here so they // do get initialized. - getBatteryMeterViewController().init(); + if (!NewStatusBarIcons.isEnabled()) { + getBatteryMeterViewController().init(); + } getHeadsUpAppearanceController().init(); getPhoneStatusBarViewController().init(); if (!NotificationsLiveDataStoreRefactor.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt index 903844efa3f0..9665c33ac4c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt @@ -16,7 +16,10 @@ package com.android.systemui.statusbar.pipeline.battery.ui.binder +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible @@ -27,6 +30,8 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark import com.android.systemui.statusbar.pipeline.battery.ui.composable.UnifiedBattery import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_HEIGHT +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_WIDTH import kotlinx.coroutines.flow.Flow /** In cases where the battery needs to be bound to an existing android view */ @@ -47,7 +52,13 @@ object UnifiedBatteryViewBinder { ) setContent { val isDark by isAreaDark.collectAsStateWithLifecycle(IsAreaDark { true }) - UnifiedBattery(viewModelFactory = viewModelFactory, isDark = isDark) + UnifiedBattery( + modifier = + Modifier.height(STATUS_BAR_BATTERY_HEIGHT) + .width(STATUS_BAR_BATTERY_WIDTH), + viewModelFactory = viewModelFactory, + isDark = isDark, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt index 2ee86ee0a679..ac793a9c97e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt @@ -54,7 +54,7 @@ fun BatteryWithEstimate( ) if (showEstimate) { viewModel.batteryTimeRemainingEstimate?.let { - Spacer(modifier.width(2.dp)) + Spacer(modifier.width(4.dp)) Text(text = it, color = Color.White) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt index d0d099e74cb1..afd4bb1f36c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.battery.ui.viewmodel import android.content.Context import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.dp import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.lifecycle.ExclusiveActivatable @@ -223,6 +224,10 @@ constructor(interactor: BatteryInteractor, @Application context: Context) : Excl } companion object { + // Status bar battery height, based on a 21x12 base canvas + val STATUS_BAR_BATTERY_HEIGHT = 13.dp + val STATUS_BAR_BATTERY_WIDTH = 22.75.dp + fun Int.glyphRepresentation(): List<BatteryGlyph> = toString().map { it.toGlyph() } private fun Char.toGlyph(): BatteryGlyph = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 9d72daf01831..c34fa464cc3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -23,6 +23,8 @@ import android.widget.LinearLayout import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,9 +36,11 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme import com.android.keyguard.AlphaOptimizedLinearLayout +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor @@ -51,6 +55,9 @@ import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ui.DarkIconManager import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.pipeline.battery.ui.composable.UnifiedBattery +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_HEIGHT +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.STATUS_BAR_BATTERY_WIDTH import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIconBlockListBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel @@ -73,14 +80,13 @@ constructor( ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) - val displayId = root.context.displayId val darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView composeView.apply { setContent { StatusBarRoot( parent = root, - statusBarViewModel = homeStatusBarViewModelFactory.create(displayId), + statusBarViewModelFactory = homeStatusBarViewModelFactory, statusBarViewBinder = homeStatusBarViewBinder, notificationIconsBinder = notificationIconsBinder, darkIconManagerFactory = darkIconManagerFactory, @@ -110,7 +116,7 @@ constructor( @Composable fun StatusBarRoot( parent: ViewGroup, - statusBarViewModel: HomeStatusBarViewModel, + statusBarViewModelFactory: HomeStatusBarViewModelFactory, statusBarViewBinder: HomeStatusBarViewBinder, notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, darkIconManagerFactory: DarkIconManager.Factory, @@ -120,6 +126,10 @@ fun StatusBarRoot( eventAnimationInteractor: SystemStatusEventAnimationInteractor, onViewCreated: (ViewGroup) -> Unit, ) { + val displayId = parent.context.displayId + val statusBarViewModel = + rememberViewModel("HomeStatusBar") { statusBarViewModelFactory.create(displayId) } + Box(Modifier.fillMaxSize()) { // TODO(b/364360986): remove this before rolling the flag forward if (StatusBarRootModernization.SHOW_DISAMBIGUATION) { @@ -159,10 +169,6 @@ fun StatusBarRoot( LinearLayout.LayoutParams.WRAP_CONTENT, ) - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - setContent { PlatformTheme { val chips by @@ -241,6 +247,12 @@ fun StatusBarRoot( endSideContent.addView(composeView, 0) } + // If the flag is enabled, create and add a compose battery view to the end + // of the system_icons container + if (NewStatusBarIcons.isEnabled) { + addBatteryComposable(phoneStatusBarView, statusBarViewModel) + } + notificationIconsBinder.bindWhileAttached( notificationIconContainer, context.displayId, @@ -263,6 +275,27 @@ fun StatusBarRoot( } } +/** Create a new [UnifiedBattery] and add it to the end of the system_icons container */ +private fun addBatteryComposable( + phoneStatusBarView: PhoneStatusBarView, + statusBarViewModel: HomeStatusBarViewModel, +) { + val batteryComposeView = + ComposeView(phoneStatusBarView.context).apply { + setContent { + UnifiedBattery( + modifier = + Modifier.height(STATUS_BAR_BATTERY_HEIGHT).width(STATUS_BAR_BATTERY_WIDTH), + viewModelFactory = statusBarViewModel.batteryViewModelFactory, + isDark = statusBarViewModel.areaDark, + ) + } + } + phoneStatusBarView.findViewById<ViewGroup>(R.id.system_icons).apply { + addView(batteryComposeView, -1) + } +} + /** * This is our analog of the flexi "ribbon", which just shows some text so we know if the flag is on */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index 1bc45a95044c..f396cbfc8000 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.annotation.ColorInt import android.graphics.Rect import android.view.View +import androidx.compose.runtime.getValue import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -28,6 +29,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.plugins.DarkIconDispatcher @@ -55,8 +58,10 @@ import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNoti import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor +import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor import com.android.systemui.statusbar.pipeline.shared.ui.model.SystemInfoCombinedVisibilityModel @@ -90,6 +95,9 @@ import kotlinx.coroutines.flow.stateIn * so that it's all in one place and easily testable outside of the fragment. */ interface HomeStatusBarViewModel { + /** Factory to create the view model for the battery icon */ + val batteryViewModelFactory: BatteryViewModel.Factory + /** * True if the device is currently transitioning from lockscreen to occluded and false * otherwise. @@ -171,6 +179,9 @@ interface HomeStatusBarViewModel { */ val areaTint: Flow<StatusBarTintColor> + /** [IsAreaDark] applicable for this status bar's display and content area */ + val areaDark: IsAreaDark + /** Interface for the assisted factory, to allow for providing a fake in tests */ interface HomeStatusBarViewModelFactory { fun create(displayId: Int): HomeStatusBarViewModel @@ -181,6 +192,7 @@ class HomeStatusBarViewModelImpl @AssistedInject constructor( @Assisted thisDisplayId: Int, + override val batteryViewModelFactory: BatteryViewModel.Factory, tableLoggerFactory: TableLogBufferFactory, homeStatusBarInteractor: HomeStatusBarInteractor, homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor, @@ -201,7 +213,9 @@ constructor( statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Background bgScope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, -) : HomeStatusBarViewModel { +) : HomeStatusBarViewModel, ExclusiveActivatable() { + + private val hydrator = Hydrator(traceName = "HomeStatusBarViewModel.hydrator") val tableLogger = tableLoggerFactory.getOrCreate(tableLogBufferName(thisDisplayId), 200) @@ -294,6 +308,13 @@ constructor( .distinctUntilChanged() .flowOn(bgDispatcher) + override val areaDark: IsAreaDark by + hydrator.hydratedStateOf( + traceName = "areaDark", + initialValue = IsAreaDark { true }, + source = darkIconInteractor.isAreaDark(thisDisplayId), + ) + /** * True if the current SysUI state can show the home status bar (aka this status bar), and false * if we shouldn't be showing any part of the home status bar. @@ -473,6 +494,10 @@ constructor( @View.Visibility private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + /** Inject this to create the display-dependent view model */ @AssistedFactory interface HomeStatusBarViewModelFactoryImpl : diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index cd401d5deb6e..e1640cd4ce7a 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -214,7 +214,12 @@ constructor( private suspend fun waitForScreenTurnedOn() { traceAsync(TAG, "waitForScreenTurnedOn()") { - powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + // dropping first as it's stateFlow and will always emit latest value but we're + // only interested in new states + powerInteractor.screenPowerState + .drop(1) + .filter { it == ScreenPowerState.SCREEN_ON } + .first() } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt index 6ac0bb168f18..91f142646c3d 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt @@ -50,6 +50,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest @@ -160,7 +161,12 @@ constructor( private suspend fun waitForScreenTurnedOn() { traceAsync(TAG, "waitForScreenTurnedOn()") { - powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + // dropping first as it's stateFlow and will always emit latest value but we're + // only interested in new states + powerInteractor.screenPowerState + .drop(1) + .filter { it == ScreenPowerState.SCREEN_ON } + .first() } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 68bffeefb0f0..4d5477052388 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -37,8 +37,6 @@ import android.media.IAudioService; import android.media.IVolumeController; import android.media.MediaRouter2Manager; import android.media.VolumePolicy; -import android.media.session.MediaController.PlaybackInfo; -import android.media.session.MediaSession.Token; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; @@ -61,6 +59,7 @@ import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; import com.android.settingslib.volume.MediaSessions; +import com.android.settingslib.volume.MediaSessions.SessionId; import com.android.systemui.Dumpable; import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -1402,12 +1401,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks { - private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); + private final HashMap<SessionId, Integer> mRemoteStreams = new HashMap<>(); private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; @Override - public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) { + public void onRemoteUpdate( + SessionId token, String name, MediaSessions.VolumeInfo volumeInfo) { addStream(token, "onRemoteUpdate"); int stream = 0; @@ -1415,14 +1415,15 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa stream = mRemoteStreams.get(token); } Slog.d(TAG, - "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume()); + "onRemoteUpdate: stream: " + + stream + " volume: " + volumeInfo.getCurrentVolume()); boolean changed = mState.states.indexOfKey(stream) < 0; final StreamState ss = streamStateW(stream); ss.dynamic = true; ss.levelMin = 0; - ss.levelMax = pi.getMaxVolume(); - if (ss.level != pi.getCurrentVolume()) { - ss.level = pi.getCurrentVolume(); + ss.levelMax = volumeInfo.getMaxVolume(); + if (ss.level != volumeInfo.getCurrentVolume()) { + ss.level = volumeInfo.getCurrentVolume(); changed = true; } if (!Objects.equals(ss.remoteLabel, name)) { @@ -1437,11 +1438,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override - public void onRemoteVolumeChanged(Token token, int flags) { - addStream(token, "onRemoteVolumeChanged"); + public void onRemoteVolumeChanged(SessionId sessionId, int flags) { + addStream(sessionId, "onRemoteVolumeChanged"); int stream = 0; synchronized (mRemoteStreams) { - stream = mRemoteStreams.get(token); + stream = mRemoteStreams.get(sessionId); } final boolean showUI = shouldShowUI(flags); Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI); @@ -1459,7 +1460,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override - public void onRemoteRemoved(Token token) { + public void onRemoteRemoved(SessionId token) { int stream; synchronized (mRemoteStreams) { if (!mRemoteStreams.containsKey(token)) { @@ -1480,7 +1481,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } public void setStreamVolume(int stream, int level) { - final Token token = findToken(stream); + final SessionId token = findToken(stream); if (token == null) { Log.w(TAG, "setStreamVolume: No token found for stream: " + stream); return; @@ -1488,9 +1489,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mMediaSessions.setVolume(token, level); } - private Token findToken(int stream) { + private SessionId findToken(int stream) { synchronized (mRemoteStreams) { - for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) { + for (Map.Entry<SessionId, Integer> entry : mRemoteStreams.entrySet()) { if (entry.getValue().equals(stream)) { return entry.getKey(); } @@ -1499,7 +1500,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa return null; } - private void addStream(Token token, String triggeringMethod) { + private void addStream(SessionId token, String triggeringMethod) { synchronized (mRemoteStreams) { if (!mRemoteStreams.containsKey(token)) { mRemoteStreams.put(token, mNextStream); diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 83b7c1818341..86defff4a120 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -68,7 +68,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.volume_dialog) - requireViewById<View>(R.id.volume_dialog_root).repeatWhenAttached { + requireViewById<View>(R.id.volume_dialog).repeatWhenAttached { coroutineScopeTraced("[Volume]dialog") { val component = componentFactory.create(this) with(component.volumeDialogViewBinder()) { bind(this@VolumeDialog) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt index 20a74b027db5..afe3d7bf217a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt @@ -17,7 +17,9 @@ package com.android.systemui.volume.dialog.domain.interactor import android.annotation.SuppressLint +import android.provider.Settings import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope @@ -28,8 +30,9 @@ import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityMod import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible import com.android.systemui.volume.dialog.utils.VolumeTracer import javax.inject.Inject -import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -43,8 +46,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds - /** * Handles Volume Dialog visibility state. It might change from several sources: * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change; @@ -60,8 +61,11 @@ constructor( private val tracer: VolumeTracer, private val repository: VolumeDialogVisibilityRepository, private val controller: VolumeDialogController, + private val secureSettingsRepository: SecureSettingsRepository, ) { + private val defaultTimeout = 3.seconds + @SuppressLint("SharedFlowCreation") private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1) val dialogVisibility: Flow<VolumeDialogVisibilityModel> = @@ -73,7 +77,14 @@ constructor( init { merge( mutableDismissDialogEvents.mapLatest { - delay(MAX_DIALOG_SHOW_TIME) + delay( + secureSettingsRepository + .getInt( + Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, + defaultTimeout.toInt(DurationUnit.MILLISECONDS), + ) + .milliseconds + ) VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT) }, callbacksInteractor.event, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index 3d0c7d64b2a4..92ec4f554548 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -246,16 +246,12 @@ constructor( uiModel.drawerState.currentMode != uiModel.drawerState.previousMode ) { val count = uiModel.availableButtons.size - val selectedButton = - getChildAt(count - uiModel.currentButtonIndex) - .requireViewById<ImageButton>(R.id.volume_drawer_button) + val selectedButton = getChildAt(count - uiModel.currentButtonIndex) as ImageButton val previousIndex = uiModel.availableButtons.indexOfFirst { it.ringerMode == uiModel.drawerState.previousMode } - val unselectedButton = - getChildAt(count - previousIndex) - .requireViewById<ImageButton>(R.id.volume_drawer_button) + val unselectedButton = getChildAt(count - previousIndex) as ImageButton // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. @@ -306,7 +302,7 @@ constructor( ) { val count = uiModel.availableButtons.size uiModel.availableButtons.fastForEachIndexed { index, ringerButton -> - val view = getChildAt(count - index) + val view = getChildAt(count - index) as ImageButton val isOpen = uiModel.drawerState is RingerDrawerState.Open if (index == uiModel.currentButtonIndex) { view.bindDrawerButton( @@ -323,37 +319,37 @@ constructor( onAnimationEnd?.run() } - private fun View.bindDrawerButton( + private fun ImageButton.bindDrawerButton( buttonViewModel: RingerButtonViewModel, viewModel: VolumeDialogRingerDrawerViewModel, isOpen: Boolean, isSelected: Boolean = false, isAnimated: Boolean = false, ) { + // id = buttonViewModel.viewId + setSelected(isSelected) val ringerContentDesc = context.getString(buttonViewModel.contentDescriptionResId) - with(requireViewById<ImageButton>(R.id.volume_drawer_button)) { - setImageResource(buttonViewModel.imageResId) - contentDescription = - if (isSelected && !isOpen) { - context.getString( - R.string.volume_ringer_drawer_closed_content_description, - ringerContentDesc, - ) - } else { - ringerContentDesc - } - if (isSelected && !isAnimated) { - setBackgroundResource(R.drawable.volume_drawer_selection_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) - background = background.mutate() - } else if (!isAnimated) { - setBackgroundResource(R.drawable.volume_ringer_item_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) - background = background.mutate() - } - setOnClickListener { - viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected) + setImageResource(buttonViewModel.imageResId) + contentDescription = + if (isSelected && !isOpen) { + context.getString( + R.string.volume_ringer_drawer_closed_content_description, + ringerContentDesc, + ) + } else { + ringerContentDesc } + if (isSelected && !isAnimated) { + setBackgroundResource(R.drawable.volume_drawer_selection_bg) + setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) + background = background.mutate() + } else if (!isAnimated) { + setBackgroundResource(R.drawable.volume_ringer_item_bg) + setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) + background = background.mutate() + } + setOnClickListener { + viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index f2d7d956291c..7cc4bcc4e11c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -74,7 +74,7 @@ constructor( val insets: MutableStateFlow<WindowInsets> = MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. - val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) + val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog) animateVisibility(root, dialog, viewModel.dialogVisibilityModel) diff --git a/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt new file mode 100644 index 000000000000..95b3b68fa1ca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window.dagger + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import com.android.systemui.window.data.repository.WindowRootViewBlurRepositoryImpl +import dagger.Binds +import dagger.Module + +/** + * Module that can be installed in sysui variants where we support cross window blur. + */ +@Module +interface WindowRootViewBlurModule { + @Binds + @SysUISingleton + fun bindWindowRootViewBlurRepository( + windowRootViewBlurRepositoryImpl: WindowRootViewBlurRepositoryImpl + ): WindowRootViewBlurRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt new file mode 100644 index 000000000000..ae917e072ff3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/dagger/WindowRootViewBlurNotSupportedModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window.dagger + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.window.data.repository.NoopWindowRootViewBlurRepository +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import dagger.Binds +import dagger.Module + +/** + * Module that can be installed in sysui variants where we don't support cross window blur. + */ +@Module +interface WindowRootViewBlurNotSupportedModule { + @Binds + @SysUISingleton + fun bindWindowRootViewBlurRepository( + windowRootViewBlurRepositoryImpl: NoopWindowRootViewBlurRepository + ): WindowRootViewBlurRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt new file mode 100644 index 000000000000..80aa11a71569 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.window.data.repository + +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class NoopWindowRootViewBlurRepository @Inject constructor() : WindowRootViewBlurRepository { + override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) + override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(true) + override val isBlurSupported: StateFlow<Boolean> = MutableStateFlow(false) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index 22a74c86e0f1..41ceda033bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -16,14 +16,77 @@ package com.android.systemui.window.data.repository +import android.app.ActivityManager +import android.os.SystemProperties +import android.view.CrossWindowBlurListeners +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository.Companion.isDisableBlurSysPropSet +import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn /** Repository that maintains state for the window blur effect. */ +interface WindowRootViewBlurRepository { + val blurRadius: MutableStateFlow<Int> + val isBlurOpaque: MutableStateFlow<Boolean> + + /** Is blur supported based on settings toggle and battery power saver mode. */ + val isBlurSupported: StateFlow<Boolean> + + companion object { + /** + * Whether the `persist.sysui.disableBlur` is set, this is used to disable blur for tests. + */ + @JvmStatic + fun isDisableBlurSysPropSet() = SystemProperties.getBoolean(DISABLE_BLUR_PROPERTY, false) + + // property that can be used to disable the cross window blur for tests + private const val DISABLE_BLUR_PROPERTY = "persist.sysui.disableBlur" + } +} + @SysUISingleton -class WindowRootViewBlurRepository @Inject constructor() { - val blurRadius = MutableStateFlow(0) +class WindowRootViewBlurRepositoryImpl +@Inject +constructor( + crossWindowBlurListeners: CrossWindowBlurListeners, + @Main private val executor: Executor, + @Application private val scope: CoroutineScope, +) : WindowRootViewBlurRepository { + override val blurRadius = MutableStateFlow(0) + + override val isBlurOpaque = MutableStateFlow(false) + + override val isBlurSupported: StateFlow<Boolean> = + conflatedCallbackFlow { + val sendUpdate = { value: Boolean -> + trySendWithFailureLogging( + isBlurAllowed() && value, + TAG, + "unable to send blur enabled/disable state change", + ) + } + crossWindowBlurListeners.addListener(executor, sendUpdate) + sendUpdate(crossWindowBlurListeners.isCrossWindowBlurEnabled) + + awaitClose { crossWindowBlurListeners.removeListener(sendUpdate) } + } // stateIn because this is backed by a binder call. + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + private fun isBlurAllowed(): Boolean { + return ActivityManager.isHighEndGfx() && !isDisableBlurSysPropSet() + } - val isBlurOpaque = MutableStateFlow(false) + companion object { + const val TAG = "WindowRootViewBlurRepository" + } } diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt index 9e369347dea5..7a88a2ef966b 100644 --- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt @@ -75,6 +75,12 @@ constructor( _onBlurAppliedEvent.emit(appliedBlurRadius) } + /** + * Whether blur is enabled or not based on settings toggle, critical thermal state, battery save + * state and multimedia tunneling state. + */ + val isBlurCurrentlySupported: StateFlow<Boolean> = repository.isBlurSupported + /** Radius of blur to be applied on the window root view. */ val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 732561e0979b..944604f94ce4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -640,11 +640,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } - @DisableFlags(FLAG_GLANCEABLE_HUB_V2) @Test fun onTouchEvent_shadeInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { + `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true) // On lockscreen. goToScene(CommunalScenes.Blank) whenever( @@ -721,11 +721,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } - @DisableFlags(FLAG_GLANCEABLE_HUB_V2) @Test fun onTouchEvent_bouncerInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { + `whenever`(communalViewModel.swipeToHubEnabled()).thenReturn(true) // On lockscreen. goToScene(CommunalScenes.Blank) whenever( diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt index edb0f352a64a..f3af794f776b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt @@ -59,6 +59,7 @@ import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.batteryViewModelFactory import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.NextAlarmController @@ -202,6 +203,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { Lazy { kosmos.shadeDisplaysRepository }, variableDateViewControllerFactory, batteryMeterViewController, + kosmos.batteryViewModelFactory, dumpManager, mShadeCarrierGroupControllerBuilder, combinedShadeHeadersConstraintManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index a5234883ed77..14a1233045bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -290,7 +290,8 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardInteractor, mKosmos.getTestDispatcher(), mLinearLargeScreenShadeInterpolator, - new BlurConfig(0.0f, 0.0f)); + new BlurConfig(0.0f, 0.0f), + mKosmos::getWindowRootViewBlurInteractor); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1204,7 +1205,8 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardInteractor, mKosmos.getTestDispatcher(), mLinearLargeScreenShadeInterpolator, - new BlurConfig(0.0f, 0.0f)); + new BlurConfig(0.0f, 0.0f), + mKosmos::getWindowRootViewBlurInteractor); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 2f30b745a4a3..3190d3ae8f16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -91,11 +91,11 @@ import com.android.systemui.statusbar.notification.collection.provider.LaunchFul import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository; import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -122,7 +122,6 @@ import java.util.Optional; @TestableLooper.RunWithLooper(setAsMainLooper = true) public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { - private static final int DISPLAY_ID = 0; private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock @@ -233,7 +232,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mNotificationActivityStarter = new StatusBarNotificationActivityStarter( getContext(), - DISPLAY_ID, mHandler, mUiBgExecutor, mVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index fcdda9f13099..9da8e80283b6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.service.dream.dreamManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -44,5 +45,6 @@ var Kosmos.fromDozingTransitionInteractor by deviceEntryInteractor = deviceEntryInteractor, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, dreamManager = dreamManager, + communalSettingsInteractor = communalSettingsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index b255b51281af..044332981bf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -57,12 +57,10 @@ fun Kosmos.useUnconfinedTestDispatcher() = apply { testDispatcher = UnconfinedTe var Kosmos.testScope by Fixture { TestScope(testDispatcher) } var Kosmos.backgroundScope by Fixture { testScope.backgroundScope } -var Kosmos.applicationCoroutineScope by Fixture { backgroundScope } +var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope } var Kosmos.testCase: SysuiTestCase by Fixture() -var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { - backgroundScope.coroutineContext -} -var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testScope.coroutineContext } +var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { testDispatcher } +var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testDispatcher } /** * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 446e10671afb..60b371aa8afb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisionin import com.android.systemui.statusbar.ui.viewmodel.keyguardStatusBarViewModel import com.android.systemui.util.time.systemClock import com.android.systemui.volume.domain.interactor.volumeDialogInteractor +import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor /** * Helper for using [Kosmos] from Java. @@ -192,4 +193,5 @@ class KosmosJavaAdapter() { val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor } val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } val mockWindowRootViewProvider by lazy { kosmos.mockWindowRootViewProvider } + val windowRootViewBlurInteractor by lazy { kosmos.windowRootViewBlurInteractor } } 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 index 5fc31f8b9e10..f2871149de11 100644 --- 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 @@ -19,6 +19,7 @@ package com.android.systemui.qs.pipeline.data.repository import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.TilesUpgradePath import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -79,9 +80,9 @@ class FakeTileSpecRepository( with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles } } - override val tilesReadFromSetting: Channel<Pair<Set<TileSpec>, Int>> = Channel(capacity = 10) + override val tilesUpgradePath: Channel<Pair<TilesUpgradePath, Int>> = Channel(capacity = 10) - suspend fun sendTilesReadFromSetting(tiles: Set<TileSpec>, userId: Int) { - tilesReadFromSetting.send(tiles to userId) + suspend fun sendTilesFromUpgradePath(upgradePath: TilesUpgradePath, userId: Int) { + tilesUpgradePath.send(upgradePath to userId) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt index 5ff44e5d33c5..c5de02a7281b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt @@ -26,7 +26,7 @@ val Kosmos.minimumTilesRepository: MinimumTilesRepository by Kosmos.Fixture { fakeMinimumTilesRepository } var Kosmos.fakeDefaultTilesRepository by Kosmos.Fixture { FakeDefaultTilesRepository() } -val Kosmos.defaultTilesRepository: DefaultTilesRepository by +var Kosmos.defaultTilesRepository: DefaultTilesRepository by Kosmos.Fixture { fakeDefaultTilesRepository } val Kosmos.fakeTileSpecRepository by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt index 0d6ac4481742..d787e2c190c8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt @@ -52,7 +52,6 @@ val Kosmos.statusBarNotificationActivityStarter by Kosmos.Fixture { StatusBarNotificationActivityStarter( applicationContext, - applicationContext.displayId, fakeExecutorHandler, fakeExecutor, notificationVisibilityProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt index c6cf0063986a..7dd0103e9f5a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt @@ -22,3 +22,10 @@ import com.android.systemui.statusbar.pipeline.battery.domain.interactor.battery val Kosmos.batteryViewModel by Kosmos.Fixture { BatteryViewModel(batteryInteractor, testableContext) } + +val Kosmos.batteryViewModelFactory by + Kosmos.Fixture { + object : BatteryViewModel.Factory { + override fun create(): BatteryViewModel = batteryViewModel + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index bc29dba3442c..fbada934c9d4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.activeNotif import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor import com.android.systemui.statusbar.phone.domain.interactor.lightsOutInteractor +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.batteryViewModelFactory import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStatusBarInteractor @@ -42,6 +43,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by Kosmos.Fixture { HomeStatusBarViewModelImpl( testableContext.displayId, + batteryViewModelFactory, tableLogBufferFactory, homeStatusBarInteractor, homeStatusBarIconBlockListInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt index 0d2aa4c79753..888b7e625524 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository import com.android.systemui.volume.dialog.utils.volumeTracer @@ -30,5 +31,6 @@ val Kosmos.volumeDialogVisibilityInteractor by volumeTracer, volumeDialogVisibilityRepository, volumeDialogController, + secureSettingsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt index 7281e03a5ea4..96992233375d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt @@ -17,5 +17,16 @@ package com.android.systemui.window.data.repository import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.flow.MutableStateFlow -val Kosmos.windowRootViewBlurRepository by Kosmos.Fixture { WindowRootViewBlurRepository() } +val Kosmos.fakeWindowRootViewBlurRepository: FakeWindowRootViewBlurRepository by + Kosmos.Fixture { FakeWindowRootViewBlurRepository() } + +val Kosmos.windowRootViewBlurRepository: WindowRootViewBlurRepository by + Kosmos.Fixture { fakeWindowRootViewBlurRepository } + +class FakeWindowRootViewBlurRepository : WindowRootViewBlurRepository { + override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) + override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isBlurSupported: MutableStateFlow<Boolean> = MutableStateFlow(false) +} diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index db8441d2424b..5283df5ca7e1 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -17,11 +17,16 @@ package com.android.server.accessibility.autoclick; import static android.view.MotionEvent.BUTTON_PRIMARY; +import static android.view.MotionEvent.BUTTON_SECONDARY; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_DELAY_DEFAULT; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT; import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface; import android.accessibilityservice.AccessibilityTrace; import android.annotation.NonNull; @@ -84,6 +89,23 @@ public class AutoclickController extends BaseEventStreamTransformation { @VisibleForTesting AutoclickTypePanel mAutoclickTypePanel; private WindowManager mWindowManager; + // Default click type is left-click. + private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; + + @VisibleForTesting + final ClickPanelControllerInterface clickPanelController = + new ClickPanelControllerInterface() { + @Override + public void handleAutoclickTypeChange(@AutoclickType int clickType) { + mActiveClickType = clickType; + } + + @Override + public void toggleAutoclickPause() { + // TODO(b/388872274): allows users to pause the autoclick. + } + }; + public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) { mTrace = trace; mContext = context; @@ -124,7 +146,8 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickIndicatorView = new AutoclickIndicatorView(mContext); mWindowManager = mContext.getSystemService(WindowManager.class); - mAutoclickTypePanel = new AutoclickTypePanel(mContext, mWindowManager); + mAutoclickTypePanel = + new AutoclickTypePanel(mContext, mWindowManager, clickPanelController); mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); @@ -644,6 +667,15 @@ public class AutoclickController extends BaseEventStreamTransformation { final long now = SystemClock.uptimeMillis(); + // TODO(b/395094903): always triggers left-click when the cursor hovers over the + // autoclick type panel, to always allow users to change a different click type. + // Otherwise, if one chooses the right-click, this user won't be able to rely on + // autoclick to select other click types. + final int actionButton = + mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK + ? BUTTON_SECONDARY + : BUTTON_PRIMARY; + MotionEvent downEvent = MotionEvent.obtain( /* downTime= */ now, @@ -653,7 +685,7 @@ public class AutoclickController extends BaseEventStreamTransformation { mTempPointerProperties, mTempPointerCoords, mMetaState, - BUTTON_PRIMARY, + actionButton, /* xPrecision= */ 1.0f, /* yPrecision= */ 1.0f, mLastMotionEvent.getDeviceId(), @@ -663,11 +695,11 @@ public class AutoclickController extends BaseEventStreamTransformation { MotionEvent pressEvent = MotionEvent.obtain(downEvent); pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS); - pressEvent.setActionButton(BUTTON_PRIMARY); + pressEvent.setActionButton(actionButton); MotionEvent releaseEvent = MotionEvent.obtain(downEvent); releaseEvent.setAction(MotionEvent.ACTION_BUTTON_RELEASE); - releaseEvent.setActionButton(BUTTON_PRIMARY); + releaseEvent.setActionButton(actionButton); releaseEvent.setButtonState(0); MotionEvent upEvent = MotionEvent.obtain(downEvent); diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index 2ef11f4b78e1..cf928e2f3fa4 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import android.annotation.IntDef; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; @@ -39,12 +40,40 @@ public class AutoclickTypePanel { private final String TAG = AutoclickTypePanel.class.getSimpleName(); + public static final int AUTOCLICK_TYPE_LEFT_CLICK = 0; + public static final int AUTOCLICK_TYPE_RIGHT_CLICK = 1; + public static final int AUTOCLICK_TYPE_DOUBLE_CLICK = 2; + public static final int AUTOCLICK_TYPE_DRAG = 3; + public static final int AUTOCLICK_TYPE_SCROLL = 4; + + // Types of click the AutoclickTypePanel supports. + @IntDef({ + AUTOCLICK_TYPE_LEFT_CLICK, + AUTOCLICK_TYPE_RIGHT_CLICK, + AUTOCLICK_TYPE_DOUBLE_CLICK, + AUTOCLICK_TYPE_DRAG, + AUTOCLICK_TYPE_SCROLL, + }) + public @interface AutoclickType {} + + // An interface exposed to {@link AutoclickController) to handle different actions on the panel, + // including changing autoclick type, pausing/resuming autoclick. + public interface ClickPanelControllerInterface { + // Allows users to change a different autoclick type. + void handleAutoclickTypeChange(@AutoclickType int clickType); + + // Allows users to pause/resume the autoclick. + void toggleAutoclickPause(); + } + private final Context mContext; private final View mContentView; private final WindowManager mWindowManager; + private final ClickPanelControllerInterface mClickPanelController; + // Whether the panel is expanded or not. private boolean mExpanded = false; @@ -56,9 +85,13 @@ public class AutoclickTypePanel { private LinearLayout mSelectedButton; - public AutoclickTypePanel(Context context, WindowManager windowManager) { + public AutoclickTypePanel( + Context context, + WindowManager windowManager, + ClickPanelControllerInterface clickPanelController) { mContext = context; mWindowManager = windowManager; + mClickPanelController = clickPanelController; mContentView = LayoutInflater.from(context) @@ -76,26 +109,35 @@ public class AutoclickTypePanel { } private void initializeButtonState() { - mLeftClickButton.setOnClickListener(v -> togglePanelExpansion(mLeftClickButton)); - mRightClickButton.setOnClickListener(v -> togglePanelExpansion(mRightClickButton)); - mDoubleClickButton.setOnClickListener(v -> togglePanelExpansion(mDoubleClickButton)); - mScrollButton.setOnClickListener(v -> togglePanelExpansion(mScrollButton)); - mDragButton.setOnClickListener(v -> togglePanelExpansion(mDragButton)); + mLeftClickButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_LEFT_CLICK)); + mRightClickButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_RIGHT_CLICK)); + mDoubleClickButton.setOnClickListener( + v -> togglePanelExpansion(AUTOCLICK_TYPE_DOUBLE_CLICK)); + mScrollButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_SCROLL)); + mDragButton.setOnClickListener(v -> togglePanelExpansion(AUTOCLICK_TYPE_DRAG)); + + // TODO(b/388872274): registers listener for pause button and allows users to pause/resume + // the autoclick. + // TODO(b/388847771): registers listener for position button and allows users to move the + // panel to a different position. // Initializes panel as collapsed state and only displays the left click button. hideAllClickTypeButtons(); mLeftClickButton.setVisibility(View.VISIBLE); - setSelectedButton(/* selectedButton= */ mLeftClickButton); + setSelectedClickType(AUTOCLICK_TYPE_LEFT_CLICK); } /** Sets the selected button and updates the newly and previously selected button styling. */ - private void setSelectedButton(@NonNull LinearLayout selectedButton) { + private void setSelectedClickType(@AutoclickType int clickType) { + final LinearLayout selectedButton = getButtonFromClickType(clickType); + // Updates the previously selected button styling. if (mSelectedButton != null) { toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false); } mSelectedButton = selectedButton; + mClickPanelController.handleAutoclickTypeChange(clickType); // Updates the newly selected button styling. toggleSelectedButtonStyle(selectedButton, /* isSelected= */ true); @@ -130,7 +172,9 @@ public class AutoclickTypePanel { } /** Toggles the panel expanded or collapsed state. */ - private void togglePanelExpansion(LinearLayout button) { + private void togglePanelExpansion(@AutoclickType int clickType) { + final LinearLayout button = getButtonFromClickType(clickType); + if (mExpanded) { // If the panel is already in expanded state, we should collapse it by hiding all // buttons except the one user selected. @@ -138,7 +182,7 @@ public class AutoclickTypePanel { button.setVisibility(View.VISIBLE); // Sets the newly selected button. - setSelectedButton(/* selectedButton= */ button); + setSelectedClickType(clickType); } else { // If the panel is already collapsed, we just need to expand it. showAllClickTypeButtons(); @@ -166,6 +210,17 @@ public class AutoclickTypePanel { mScrollButton.setVisibility(View.VISIBLE); } + private LinearLayout getButtonFromClickType(@AutoclickType int clickType) { + return switch (clickType) { + case AUTOCLICK_TYPE_LEFT_CLICK -> mLeftClickButton; + case AUTOCLICK_TYPE_RIGHT_CLICK -> mRightClickButton; + case AUTOCLICK_TYPE_DOUBLE_CLICK -> mDoubleClickButton; + case AUTOCLICK_TYPE_DRAG -> mDragButton; + case AUTOCLICK_TYPE_SCROLL -> mScrollButton; + default -> throw new IllegalArgumentException("Unknown clickType " + clickType); + }; + } + @VisibleForTesting boolean getExpansionStateForTesting() { return mExpanded; diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 414db37508e5..05301fdd8385 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -588,10 +588,10 @@ public class CompanionDeviceManagerService extends SystemService { @Override @EnforcePermission(DELIVER_COMPANION_MESSAGES) public void attachSystemDataTransport(String packageName, int userId, int associationId, - ParcelFileDescriptor fd) { + ParcelFileDescriptor fd, int flags) { attachSystemDataTransport_enforcePermission(); - mTransportManager.attachSystemDataTransport(associationId, fd); + mTransportManager.attachSystemDataTransport(associationId, fd, flags); } @Override diff --git a/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java b/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java index df3071e08a03..42af0597b35c 100644 --- a/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java +++ b/services/companion/java/com/android/server/companion/securechannel/AttestationVerifier.java @@ -16,7 +16,9 @@ package com.android.server.companion.securechannel; +import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF; import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE; +import static android.security.attestationverification.AttestationVerificationManager.PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS; import static android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE; import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE; @@ -34,15 +36,21 @@ import java.util.function.BiConsumer; /** * Helper class to perform attestation verification synchronously. + * + * @hide */ public class AttestationVerifier { private static final long ATTESTATION_VERIFICATION_TIMEOUT_SECONDS = 10; // 10 seconds private static final String PARAM_OWNED_BY_SYSTEM = "android.key_owned_by_system"; + private static final int EXTENDED_PATCH_LEVEL_DIFF_MONTHS = 24; // 2 years + private final Context mContext; + private final int mFlags; - AttestationVerifier(Context context) { + AttestationVerifier(Context context, int flags) { this.mContext = context; + this.mFlags = flags; } /** @@ -59,10 +67,13 @@ public class AttestationVerifier { @NonNull byte[] remoteAttestation, @NonNull byte[] attestationChallenge ) throws SecureChannelException { - Bundle requirements = new Bundle(); + final Bundle requirements = new Bundle(); requirements.putByteArray(PARAM_CHALLENGE, attestationChallenge); requirements.putBoolean(PARAM_OWNED_BY_SYSTEM, true); // Custom parameter for CDM + // Apply flags to verifier requirements + updateRequirements(requirements); + // Synchronously execute attestation verification. AtomicInteger verificationResult = new AtomicInteger(0); CountDownLatch verificationFinished = new CountDownLatch(1); @@ -96,4 +107,15 @@ public class AttestationVerifier { return verificationResult.get(); } + + private void updateRequirements(Bundle requirements) { + if (mFlags == 0) { + return; + } + + if ((mFlags & TRANSPORT_FLAG_EXTEND_PATCH_DIFF) > 0) { + requirements.putInt(PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS, + EXTENDED_PATCH_LEVEL_DIFF_MONTHS); + } + } } diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java index 2d3782fb3181..6c7c9b3e073d 100644 --- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java +++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java @@ -59,6 +59,7 @@ public class SecureChannel { private final Callback mCallback; private final byte[] mPreSharedKey; private final AttestationVerifier mVerifier; + private final int mFlags; private volatile boolean mStopped; private volatile boolean mInProgress; @@ -89,7 +90,7 @@ public class SecureChannel { @NonNull Callback callback, @NonNull byte[] preSharedKey ) { - this(in, out, callback, preSharedKey, null); + this(in, out, callback, preSharedKey, null, 0); } /** @@ -100,14 +101,16 @@ public class SecureChannel { * @param out output stream from which data is sent out * @param callback subscription to received messages from the channel * @param context context for fetching the Attestation Verifier Framework system service + * @param flags flags for custom security settings on the channel */ public SecureChannel( @NonNull final InputStream in, @NonNull final OutputStream out, @NonNull Callback callback, - @NonNull Context context + @NonNull Context context, + int flags ) { - this(in, out, callback, null, new AttestationVerifier(context)); + this(in, out, callback, null, new AttestationVerifier(context, flags), flags); } public SecureChannel( @@ -115,13 +118,15 @@ public class SecureChannel { final OutputStream out, Callback callback, byte[] preSharedKey, - AttestationVerifier verifier + AttestationVerifier verifier, + int flags ) { this.mInput = in; this.mOutput = out; this.mCallback = callback; this.mPreSharedKey = preSharedKey; this.mVerifier = verifier; + this.mFlags = flags; } /** diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 36083607bfcd..92d9fb02de79 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -16,7 +16,11 @@ package com.android.server.companion.transport; +import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_PERMISSION_RESTORE; +import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF; + +import static com.android.server.companion.transport.TransportUtils.enforceAssociationCanUseTransportFlags; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -152,10 +156,14 @@ public class CompanionTransportManager { /** * Attach transport. */ - public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) { + public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd, + int flags) { Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]..."); - mAssociationStore.getAssociationWithCallerChecks(associationId); + AssociationInfo association = + mAssociationStore.getAssociationWithCallerChecks(associationId); + + enforceAssociationCanUseTransportFlags(association, flags); synchronized (mTransports) { if (mTransports.contains(associationId)) { @@ -163,7 +171,7 @@ public class CompanionTransportManager { } // TODO: Implement new API to pass a PSK - initializeTransport(associationId, fd, null); + initializeTransport(association, fd, null, flags); notifyOnTransportsChanged(); } @@ -217,10 +225,12 @@ public class CompanionTransportManager { } } - private void initializeTransport(int associationId, + private void initializeTransport(AssociationInfo association, ParcelFileDescriptor fd, - byte[] preSharedKey) { + byte[] preSharedKey, + int flags) { Slog.i(TAG, "Initializing transport"); + int associationId = association.getId(); Transport transport; if (!isSecureTransportEnabled()) { // If secure transport is explicitly disabled for testing, use raw transport @@ -230,15 +240,21 @@ public class CompanionTransportManager { // If device is debug build, use hardcoded test key for authentication Slog.d(TAG, "Creating an unauthenticated secure channel"); final byte[] testKey = "CDM".getBytes(StandardCharsets.UTF_8); - transport = new SecureTransport(associationId, fd, mContext, testKey, null); + transport = new SecureTransport(associationId, fd, mContext, testKey, null, 0); } else if (preSharedKey != null) { // If either device is not Android, then use app-specific pre-shared key Slog.d(TAG, "Creating a PSK-authenticated secure channel"); - transport = new SecureTransport(associationId, fd, mContext, preSharedKey, null); + transport = new SecureTransport(associationId, fd, mContext, preSharedKey, null, 0); + } else if (DEVICE_PROFILE_WEARABLE_SENSING.equals(association.getDeviceProfile())) { + // If device is glasses with WEARABLE_SENSING profile, extend the allowed patch + // difference to 2 years instead of 1. + Slog.d(TAG, "Creating a secure channel with extended patch difference allowance"); + transport = new SecureTransport(associationId, fd, mContext, + TRANSPORT_FLAG_EXTEND_PATCH_DIFF); } else { // If none of the above applies, then use secure channel with attestation verification Slog.d(TAG, "Creating a secure channel"); - transport = new SecureTransport(associationId, fd, mContext); + transport = new SecureTransport(associationId, fd, mContext, flags); } addMessageListenersToTransport(transport); diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index 1e95e65848a5..77dc80998e2e 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -36,15 +36,22 @@ class SecureTransport extends Transport implements SecureChannel.Callback { private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500); - SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) { + SecureTransport(int associationId, ParcelFileDescriptor fd, Context context, int flags) { super(associationId, fd, context); - mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, context); + mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, context, flags); } SecureTransport(int associationId, ParcelFileDescriptor fd, Context context, - byte[] preSharedKey, AttestationVerifier verifier) { + byte[] preSharedKey, AttestationVerifier verifier, int flags) { super(associationId, fd, context); - mSecureChannel = new SecureChannel(mRemoteIn, mRemoteOut, this, preSharedKey, verifier); + mSecureChannel = new SecureChannel( + mRemoteIn, + mRemoteOut, + this, + preSharedKey, + verifier, + flags + ); } @Override diff --git a/services/companion/java/com/android/server/companion/transport/TransportUtils.java b/services/companion/java/com/android/server/companion/transport/TransportUtils.java new file mode 100644 index 000000000000..7a15c11afd19 --- /dev/null +++ b/services/companion/java/com/android/server/companion/transport/TransportUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.transport; + +import static android.companion.AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING; +import static android.companion.CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF; + +import static java.util.Collections.unmodifiableMap; + +import android.companion.AssociationInfo; +import android.util.ArrayMap; + +import java.util.Map; + +/** + * Utility class for transport manager. + * @hide + */ +public final class TransportUtils { + + /** + * Device profile -> Union of allowlisted transport flags + */ + private static final Map<String, Integer> DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST; + static { + final Map<String, Integer> map = new ArrayMap<>(); + map.put(DEVICE_PROFILE_WEARABLE_SENSING, + TRANSPORT_FLAG_EXTEND_PATCH_DIFF); + DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST = unmodifiableMap(map); + } + + /** + * Enforce that the association that is trying to attach a transport with provided flags has + * one of the allowlisted device profiles that may apply the flagged features. + * + * @param association Association for which transport is being attached + * @param flags Flags for features being applied to the transport + */ + public static void enforceAssociationCanUseTransportFlags( + AssociationInfo association, int flags) { + if (flags == 0) { + return; + } + + final String deviceProfile = association.getDeviceProfile(); + if (!DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST.containsKey(deviceProfile)) { + throw new IllegalArgumentException("Association (id=" + association.getId() + + ") with device profile " + deviceProfile + " does not support the " + + "usage of transport flags."); + } + + int allowedFlags = DEVICE_PROFILE_TRANSPORT_FLAGS_ALLOWLIST.get(deviceProfile); + + // Ensure that every non-zero bits in flags are also present in allowed flags + if ((allowedFlags & flags) != flags) { + throw new IllegalArgumentException("Association (id=" + association.getId() + + ") does not have the device profile required to use at least " + + "one of the flags in this transport."); + } + } + + private TransportUtils() {} +} diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index c6338307b192..f1007e75e0af 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -186,9 +186,28 @@ public class SettingsToPropertiesMapper { "core_libraries", "crumpet", "dck_framework", + "desktop_apps", + "desktop_better_together", + "desktop_bsp", + "desktop_camera", "desktop_connectivity", + "desktop_display", + "desktop_commercial", + "desktop_firmware", + "desktop_graphics", "desktop_hwsec", + "desktop_input", + "desktop_kernel", + "desktop_ml", + "desktop_serviceability", + "desktop_oobe", + "desktop_peripherals", + "desktop_pnp", + "desktop_security", "desktop_stats", + "desktop_sysui", + "desktop_users_and_accounts", + "desktop_video", "desktop_wifi", "devoptions_settings", "game", diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index c0a97db7275b..767201125e09 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -361,6 +361,14 @@ public final class GameManagerService extends IGameManagerService.Stub { case POPULATE_GAME_MODE_SETTINGS: { removeEqualMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj); final int userId = (int) msg.obj; + synchronized (mLock) { + if (!mSettings.containsKey(userId)) { + GameManagerSettings userSettings = new GameManagerSettings( + Environment.getDataSystemDeDirectory(userId)); + mSettings.put(userId, userSettings); + userSettings.readPersistentDataLocked(); + } + } final String[] packageNames = getInstalledGamePackageNames(userId); updateConfigsForUser(userId, false /*checkGamePackage*/, packageNames); break; @@ -990,8 +998,7 @@ public final class GameManagerService extends IGameManagerService.Stub { @Override public void onUserStarting(@NonNull TargetUser user) { Slog.d(TAG, "Starting user " + user.getUserIdentifier()); - mService.onUserStarting(user, - Environment.getDataSystemDeDirectory(user.getUserIdentifier())); + mService.onUserStarting(user, /*settingDataDirOverride*/ null); } @Override @@ -1596,13 +1603,16 @@ public final class GameManagerService extends IGameManagerService.Stub { } } - void onUserStarting(@NonNull TargetUser user, File settingDataDir) { + void onUserStarting(@NonNull TargetUser user, File settingDataDirOverride) { final int userId = user.getUserIdentifier(); - synchronized (mLock) { - if (!mSettings.containsKey(userId)) { - GameManagerSettings userSettings = new GameManagerSettings(settingDataDir); - mSettings.put(userId, userSettings); - userSettings.readPersistentDataLocked(); + if (settingDataDirOverride != null) { + synchronized (mLock) { + if (!mSettings.containsKey(userId)) { + GameManagerSettings userSettings = new GameManagerSettings( + settingDataDirOverride); + mSettings.put(userId, userSettings); + userSettings.readPersistentDataLocked(); + } } } sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, EVENT_ON_USER_STARTING, diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java index c57a1f73d7d7..fd4bf2f3ef65 100644 --- a/services/core/java/com/android/server/app/GameManagerSettings.java +++ b/services/core/java/com/android/server/app/GameManagerSettings.java @@ -221,9 +221,7 @@ public class GameManagerSettings { return false; } - try { - final FileInputStream str = mSettingsFile.openRead(); - + try (FileInputStream str = mSettingsFile.openRead()) { final TypedXmlPullParser parser = Xml.resolvePullParser(str); int type; while ((type = parser.next()) != XmlPullParser.START_TAG @@ -251,7 +249,6 @@ public class GameManagerSettings { + type); } } - str.close(); } catch (XmlPullParserException | java.io.IOException e) { Slog.wtf(TAG, "Error reading game manager settings", e); return false; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6f79f7073b89..b48d0a6ed547 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -15082,11 +15082,13 @@ public class AudioService extends IAudioService.Stub final String key = "additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); - long delayMillis; - try { - delayMillis = Long.parseLong(reply.substring(key.length() + 1)); - } catch (NullPointerException e) { - delayMillis = 0; + long delayMillis = 0; + if (reply.contains(key)) { + try { + delayMillis = Long.parseLong(reply.substring(key.length() + 1)); + } catch (NullPointerException e) { + delayMillis = 0; + } } return delayMillis; } @@ -15112,11 +15114,13 @@ public class AudioService extends IAudioService.Stub final String key = "max_additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); - long delayMillis; - try { - delayMillis = Long.parseLong(reply.substring(key.length() + 1)); - } catch (NullPointerException e) { - delayMillis = 0; + long delayMillis = 0; + if (reply.contains(key)) { + try { + delayMillis = Long.parseLong(reply.substring(key.length() + 1)); + } catch (NullPointerException e) { + delayMillis = 0; + } } return delayMillis; } diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index a1e8f08db0a6..aab2760dbc66 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -122,11 +122,6 @@ public class DisplayManagerFlags { Flags.FLAG_ALWAYS_ROTATE_DISPLAY_DEVICE, Flags::alwaysRotateDisplayDevice); - private final FlagState mRefreshRateVotingTelemetry = new FlagState( - Flags.FLAG_REFRESH_RATE_VOTING_TELEMETRY, - Flags::refreshRateVotingTelemetry - ); - private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState( Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION, Flags::enablePixelAnisotropyCorrection @@ -403,10 +398,6 @@ public class DisplayManagerFlags { return mAlwaysRotateDisplayDevice.isEnabled(); } - public boolean isRefreshRateVotingTelemetryEnabled() { - return mRefreshRateVotingTelemetry.isEnabled(); - } - public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() { return mPixelAnisotropyCorrectionEnabled.isEnabled(); } @@ -626,7 +617,6 @@ public class DisplayManagerFlags { pw.println(" " + mAutoBrightnessModesFlagState); pw.println(" " + mFastHdrTransitions); pw.println(" " + mAlwaysRotateDisplayDevice); - pw.println(" " + mRefreshRateVotingTelemetry); pw.println(" " + mPixelAnisotropyCorrectionEnabled); pw.println(" " + mSensorBasedBrightnessThrottling); pw.println(" " + mIdleScreenRefreshRateTimeout); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index cc0bbde370fe..8211febade60 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -191,14 +191,6 @@ flag { } flag { - name: "refresh_rate_voting_telemetry" - namespace: "display_manager" - description: "Feature flag for enabling telemetry for refresh rate voting in DisplayManager" - bug: "310029108" - is_fixed_read_only: true -} - -flag { name: "enable_pixel_anisotropy_correction" namespace: "display_manager" description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling" diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 1dd4a9b93277..c37733b05fba 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -229,8 +229,7 @@ public class DisplayModeDirector { mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; - mVotesStatsReporter = injector.getVotesStatsReporter( - displayManagerFlags.isRefreshRateVotingTelemetryEnabled()); + mVotesStatsReporter = injector.getVotesStatsReporter(); mSupportedModesByDisplay = new SparseArray<>(); mAppSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); @@ -3141,7 +3140,7 @@ public class DisplayModeDirector { SensorManagerInternal getSensorManagerInternal(); @Nullable - VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled); + VotesStatsReporter getVotesStatsReporter(); } @VisibleForTesting @@ -3281,10 +3280,9 @@ public class DisplayModeDirector { } @Override - public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + public VotesStatsReporter getVotesStatsReporter() { // if frame rate override supported, renderRates will be ignored in mode selection - return new VotesStatsReporter(supportsFrameRateOverride(), - refreshRateVotingTelemetryEnabled); + return new VotesStatsReporter(supportsFrameRateOverride()); } private DisplayManager getDisplayManager() { diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java index 7562a525b5f6..7b579c0e0c21 100644 --- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java +++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Trace; import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.Display; import com.android.internal.util.FrameworkStatsLog; @@ -36,13 +37,11 @@ class VotesStatsReporter { private static final String TAG = "VotesStatsReporter"; private static final int REFRESH_RATE_NOT_LIMITED = 1000; private final boolean mIgnoredRenderRate; - private final boolean mFrameworkStatsLogReportingEnabled; - private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1; + private final SparseIntArray mLastMinPriorityByDisplay = new SparseIntArray(); - public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) { + VotesStatsReporter(boolean ignoreRenderRate) { mIgnoredRenderRate = ignoreRenderRate; - mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled; } void reportVoteChanged(int displayId, int priority, @Nullable Vote vote) { @@ -57,32 +56,27 @@ class VotesStatsReporter { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate); - if (mFrameworkStatsLogReportingEnabled) { - FrameworkStatsLog.write( - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, - maxRefreshRate, -1); - } + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, + maxRefreshRate, -1); } private void reportVoteRemoved(int displayId, int priority) { Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1); - if (mFrameworkStatsLogReportingEnabled) { - FrameworkStatsLog.write( - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, - DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); - } + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); } void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode, SparseArray<Vote> votes) { - if (!mFrameworkStatsLogReportingEnabled) { - return; - } + int lastMinPriorityReported = mLastMinPriorityByDisplay.get( + displayId, Vote.MAX_PRIORITY + 1); int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1; for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) { - if (priority < mLastMinPriorityReported && priority < minPriority) { + if (priority < lastMinPriorityReported && priority < minPriority) { continue; } Vote vote = votes.get(priority); @@ -91,7 +85,7 @@ class VotesStatsReporter { } // Was previously reported ACTIVE, changed to ADDED - if (priority >= mLastMinPriorityReported && priority < minPriority) { + if (priority >= lastMinPriorityReported && priority < minPriority) { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); FrameworkStatsLog.write( DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, @@ -99,7 +93,7 @@ class VotesStatsReporter { maxRefreshRate, selectedRefreshRate); } // Was previously reported ADDED, changed to ACTIVE - if (priority >= minPriority && priority < mLastMinPriorityReported) { + if (priority >= minPriority && priority < lastMinPriorityReported) { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); FrameworkStatsLog.write( DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, @@ -107,7 +101,7 @@ class VotesStatsReporter { maxRefreshRate, selectedRefreshRate); } - mLastMinPriorityReported = minPriority; + mLastMinPriorityByDisplay.put(displayId, minPriority); } } diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig index 4505d0e2d799..7e5c1bc9ada5 100644 --- a/services/core/java/com/android/server/flags/services.aconfig +++ b/services/core/java/com/android/server/flags/services.aconfig @@ -86,3 +86,10 @@ flag { description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings" bug: "283267917" } + +flag { + name: "certpininstaller_removal" + namespace: "network_security" + description: "Remove CertPinInstallReceiver from the platform" + bug: "391205997" +} diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index ddace179348c..a04ffdb4951d 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -43,7 +43,6 @@ import com.android.internal.annotations.GuardedBy; import java.util.Collection; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; /** * A class that represents a broker for the endpoint registered by the client app. This class @@ -89,8 +88,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** The remote callback interface for this endpoint. */ private final IContextHubEndpointCallback mContextHubEndpointCallback; - /** True if this endpoint is registered with the service. */ - private AtomicBoolean mIsRegistered = new AtomicBoolean(true); + /** True if this endpoint is registered with the service/HAL. */ + @GuardedBy("mRegistrationLock") + private boolean mIsRegistered = false; + + private final Object mRegistrationLock = new Object(); private final Object mOpenSessionLock = new Object(); @@ -192,7 +194,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub public int openSession(HubEndpointInfo destination, String serviceDescriptor) throws RemoteException { super.openSession_enforcePermission(); - if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered"); + if (!isRegistered()) throw new IllegalStateException("Endpoint is not registered"); if (!hasEndpointPermissions(destination)) { throw new SecurityException( "Insufficient permission to open a session with endpoint: " + destination); @@ -223,7 +225,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void closeSession(int sessionId, int reason) throws RemoteException { super.closeSession_enforcePermission(); - if (!mIsRegistered.get()) throw new IllegalStateException("Endpoint is not registered"); + if (!isRegistered()) throw new IllegalStateException("Endpoint is not registered"); if (!cleanupSessionResources(sessionId)) { throw new IllegalArgumentException( "Unknown session ID in closeSession: id=" + sessionId); @@ -235,19 +237,26 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregister() { super.unregister_enforcePermission(); - mIsRegistered.set(false); - try { - mHubInterface.unregisterEndpoint(mHalEndpointInfo); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling HAL unregisterEndpoint", e); - } synchronized (mOpenSessionLock) { // Iterate in reverse since cleanupSessionResources will remove the entry for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { int id = mSessionInfoMap.keyAt(i); + halCloseEndpointSessionNoThrow(id, Reason.ENDPOINT_GONE); cleanupSessionResources(id); } } + synchronized (mRegistrationLock) { + if (!isRegistered()) { + Log.w(TAG, "Attempting to unregister when already unregistered"); + return; + } + mIsRegistered = false; + try { + mHubInterface.unregisterEndpoint(mHalEndpointInfo); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling HAL unregisterEndpoint", e); + } + } mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint()); releaseWakeLockOnExit(); } @@ -335,7 +344,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { - if (mIsRegistered.get()) { + if (isRegistered()) { unregister(); } } @@ -365,6 +374,22 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + /** + * Registers this endpoints with the Context Hub HAL. + * + * @throws RemoteException if the registrations fails with a RemoteException + */ + /* package */ void register() throws RemoteException { + synchronized (mRegistrationLock) { + if (isRegistered()) { + Log.w(TAG, "Attempting to register when already registered"); + } else { + mHubInterface.registerEndpoint(mHalEndpointInfo); + mIsRegistered = true; + } + } + } + /* package */ void attachDeathRecipient() throws RemoteException { if (mContextHubEndpointCallback != null) { mContextHubEndpointCallback.asBinder().linkToDeath(this, 0 /* flags */); @@ -425,6 +450,24 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + /* package */ void onHalRestart() { + synchronized (mRegistrationLock) { + mIsRegistered = false; + try { + register(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling HAL registerEndpoint", e); + } + } + synchronized (mOpenSessionLock) { + for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { + int id = mSessionInfoMap.keyAt(i); + onCloseEndpointSession(id, Reason.HUB_RESET); + } + } + // TODO(b/390029594): Cancel any ongoing reliable communication transactions + } + private Optional<Byte> onEndpointSessionOpenRequestInternal( int sessionId, HubEndpointInfo initiator, String serviceDescriptor) { if (!hasEndpointPermissions(initiator)) { @@ -553,7 +596,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private void acquireWakeLock() { Binder.withCleanCallingIdentity( () -> { - if (mIsRegistered.get()) { + if (isRegistered()) { mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); } }); @@ -608,4 +651,10 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } return true; } + + private boolean isRegistered() { + synchronized (mRegistrationLock) { + return mIsRegistered; + } + } } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index ed98bf98f7b7..06aeb62a28b8 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -206,12 +206,6 @@ import java.util.function.Consumer; EndpointInfo halEndpointInfo = ContextHubServiceUtil.createHalEndpointInfo( pendingEndpointInfo, endpointId, SERVICE_HUB_ID); - try { - mHubInterface.registerEndpoint(halEndpointInfo); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling HAL registerEndpoint", e); - throw e; - } broker = new ContextHubEndpointBroker( mContext, @@ -222,6 +216,7 @@ import java.util.function.Consumer; packageName, attributionTag, mTransactionManager); + broker.register(); mEndpointMap.put(endpointId, broker); try { @@ -282,6 +277,14 @@ import java.util.function.Consumer; mEndpointMap.remove(endpointId); } + /** Invoked by the service when the Context Hub HAL restarts. */ + /* package */ void onHalRestart() { + for (ContextHubEndpointBroker broker : mEndpointMap.values()) { + // The broker will close existing sessions and re-register itself + broker.onHalRestart(); + } + } + @Override public void onEndpointSessionOpenRequest( int sessionId, diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index 2b0ca145372b..502a7aeba258 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -259,6 +259,9 @@ public class ContextHubService extends IContextHubService.Stub { if (mHubInfoRegistry != null) { mHubInfoRegistry.onHalRestart(); } + if (mEndpointManager != null) { + mEndpointManager.onHalRestart(); + } resetSettings(); if (Flags.reconnectHostEndpointsAfterHalRestart()) { mClientManager.forEachClientOfHub(mContextHubId, diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java index e2889fa9cbf6..18bccd8411d7 100644 --- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java +++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java @@ -91,7 +91,7 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor updateAttributes = true; } if (restrictAudioAttributesAlarm() - && record.getNotification().category != CATEGORY_ALARM + && !CATEGORY_ALARM.equals(record.getNotification().category) && attributes.getUsage() == AudioAttributes.USAGE_ALARM) { updateAttributes = true; } diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java index dc1f93664f79..f060e4d11e82 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java +++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java @@ -17,8 +17,8 @@ package com.android.server.security; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_BOOT_STATE; -import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS; +import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_PATCH_LEVEL_DIFF; import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN; @@ -47,12 +47,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.server.security.AttestationVerificationManagerService.DumpLogger; -import org.json.JSONObject; - import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.cert.CertPath; import java.security.cert.CertPathValidator; @@ -60,7 +56,6 @@ import java.security.cert.CertPathValidatorException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; -import java.security.cert.PKIXCertPathChecker; import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; @@ -69,7 +64,6 @@ import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -126,6 +120,7 @@ class AttestationVerificationPeerDeviceVerifier { private final LocalDate mTestLocalPatchDate; private final CertificateFactory mCertificateFactory; private final CertPathValidator mCertPathValidator; + private final CertificateRevocationStatusManager mCertificateRevocationStatusManager; private final DumpLogger mDumpLogger; AttestationVerificationPeerDeviceVerifier(@NonNull Context context, @@ -135,6 +130,7 @@ class AttestationVerificationPeerDeviceVerifier { mCertificateFactory = CertificateFactory.getInstance("X.509"); mCertPathValidator = CertPathValidator.getInstance("PKIX"); mTrustAnchors = getTrustAnchors(); + mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext); mRevocationEnabled = true; mTestSystemDate = null; mTestLocalPatchDate = null; @@ -150,6 +146,7 @@ class AttestationVerificationPeerDeviceVerifier { mCertificateFactory = CertificateFactory.getInstance("X.509"); mCertPathValidator = CertPathValidator.getInstance("PKIX"); mTrustAnchors = trustAnchors; + mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext); mRevocationEnabled = revocationEnabled; mTestSystemDate = systemDate; mTestLocalPatchDate = localPatchDate; @@ -300,15 +297,14 @@ class AttestationVerificationPeerDeviceVerifier { CertPath certificatePath = mCertificateFactory.generateCertPath(certificates); PKIXParameters validationParams = new PKIXParameters(mTrustAnchors); + // Do not use built-in revocation status checker. + validationParams.setRevocationEnabled(false); + mCertPathValidator.validate(certificatePath, validationParams); if (mRevocationEnabled) { // Checks Revocation Status List based on // https://developer.android.com/training/articles/security-key-attestation#certificate_status - PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker(); - validationParams.addCertPathChecker(checker); + mCertificateRevocationStatusManager.checkRevocationStatus(certificates); } - // Do not use built-in revocation status checker. - validationParams.setRevocationEnabled(false); - mCertPathValidator.validate(certificatePath, validationParams); } private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException { @@ -574,96 +570,6 @@ class AttestationVerificationPeerDeviceVerifier { <= maxPatchLevelDiffMonths; } - /** - * Checks certificate revocation status. - * - * Queries status list from android.googleapis.com/attestation/status and checks for - * the existence of certificate's serial number. If serial number exists in map, then fail. - */ - private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker { - private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; - private static final String STATUS_PROPERTY_KEY = "status"; - private static final String REASON_PROPERTY_KEY = "reason"; - private String mStatusUrl; - private JSONObject mJsonStatusMap; - - @Override - public void init(boolean forward) throws CertPathValidatorException { - mStatusUrl = getRevocationListUrl(); - if (mStatusUrl == null || mStatusUrl.isEmpty()) { - throw new CertPathValidatorException( - "R.string.vendor_required_attestation_revocation_list_url is empty."); - } - // TODO(b/221067843): Update to only pull status map on non critical path and if - // out of date (24hrs). - mJsonStatusMap = getStatusMap(mStatusUrl); - } - - @Override - public boolean isForwardCheckingSupported() { - return false; - } - - @Override - public Set<String> getSupportedExtensions() { - return null; - } - - @Override - public void check(Certificate cert, Collection<String> unresolvedCritExts) - throws CertPathValidatorException { - X509Certificate x509Certificate = (X509Certificate) cert; - // The json key is the certificate's serial number converted to lowercase hex. - String serialNumber = x509Certificate.getSerialNumber().toString(16); - - if (serialNumber == null) { - throw new CertPathValidatorException("Certificate serial number can not be null."); - } - - if (mJsonStatusMap.has(serialNumber)) { - JSONObject revocationStatus; - String status; - String reason; - try { - revocationStatus = mJsonStatusMap.getJSONObject(serialNumber); - status = revocationStatus.getString(STATUS_PROPERTY_KEY); - reason = revocationStatus.getString(REASON_PROPERTY_KEY); - } catch (Throwable t) { - throw new CertPathValidatorException("Unable get properties for certificate " - + "with serial number " + serialNumber); - } - throw new CertPathValidatorException( - "Invalid certificate with serial number " + serialNumber - + " has status " + status - + " because reason " + reason); - } - } - - private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException { - URL url; - try { - url = new URL(stringUrl); - } catch (Throwable t) { - throw new CertPathValidatorException( - "Unable to get revocation status from " + mStatusUrl, t); - } - - try (InputStream inputStream = url.openStream()) { - JSONObject statusListJson = new JSONObject( - new String(inputStream.readAllBytes(), UTF_8)); - return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); - } catch (Throwable t) { - throw new CertPathValidatorException( - "Unable to parse revocation status from " + mStatusUrl, t); - } - } - - private String getRevocationListUrl() { - return mContext.getResources().getString( - R.string.vendor_required_attestation_revocation_list_url); - } - } - /* Mutable data class for tracking dump data from verifications. */ private static class MyDumpData extends AttestationVerificationManagerService.DumpData { diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java new file mode 100644 index 000000000000..d36d9f5f6636 --- /dev/null +++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.security; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Environment; +import android.os.PersistableBundle; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.cert.CertPathValidatorException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Manages the revocation status of certificates used in remote attestation. */ +class CertificateRevocationStatusManager { + private static final String TAG = "AVF_CRL"; + // Must be unique within system server + private static final int JOB_ID = 1737671340; + private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt"; + private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ","; + + /** + * The number of days since last update for which a stored revocation status can be accepted. + */ + @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30; + + /** + * The number of days since issue date for an intermediary certificate to be considered fresh + * and not require a revocation list check. + */ + private static final int FRESH_INTERMEDIARY_CERT_DAYS = 70; + + /** + * The expected number of days between a certificate's issue date and notBefore date. Used to + * infer a certificate's issue date from its notBefore date. + */ + private static final int DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES = 2; + + private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries"; + private static final Object sFileLock = new Object(); + + private final Context mContext; + private final String mTestRemoteRevocationListUrl; + private final File mTestRevocationStatusFile; + private final boolean mShouldScheduleJob; + + CertificateRevocationStatusManager(Context context) { + this(context, null, null, true); + } + + @VisibleForTesting + CertificateRevocationStatusManager( + Context context, + String testRemoteRevocationListUrl, + File testRevocationStatusFile, + boolean shouldScheduleJob) { + mContext = context; + mTestRemoteRevocationListUrl = testRemoteRevocationListUrl; + mTestRevocationStatusFile = testRevocationStatusFile; + mShouldScheduleJob = shouldScheduleJob; + } + + /** + * Check the revocation status of the provided {@link X509Certificate}s. + * + * <p>The provided certificates should have been validated and ordered from leaf to a + * certificate issued by the trust anchor, per the convention specified in the javadoc of {@link + * java.security.cert.CertPath}. + * + * @param certificates List of certificates to be checked + * @throws CertPathValidatorException if the check failed + */ + void checkRevocationStatus(List<X509Certificate> certificates) + throws CertPathValidatorException { + if (!needToCheckRevocationStatus(certificates)) { + return; + } + List<String> serialNumbers = new ArrayList<>(); + for (X509Certificate certificate : certificates) { + String serialNumber = certificate.getSerialNumber().toString(16); + if (serialNumber == null) { + throw new CertPathValidatorException("Certificate serial number cannot be null."); + } + serialNumbers.add(serialNumber); + } + try { + JSONObject revocationList = fetchRemoteRevocationList(); + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (String serialNumber : serialNumbers) { + areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber)); + } + updateLastRevocationCheckData(areCertificatesRevoked); + for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) { + if (entry.getValue()) { + throw new CertPathValidatorException( + "Certificate " + entry.getKey() + " has been revoked."); + } + } + } catch (IOException | JSONException ex) { + Slog.d(TAG, "Fallback to check stored revocation status", ex); + if (ex instanceof IOException && mShouldScheduleJob) { + scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers); + } + for (X509Certificate certificate : certificates) { + // Assume recently issued certificates are not revoked. + if (isIssuedWithinDays(certificate, MAX_DAYS_SINCE_LAST_CHECK)) { + String serialNumber = certificate.getSerialNumber().toString(16); + serialNumbers.remove(serialNumber); + } + } + Map<String, LocalDateTime> lastRevocationCheckData; + try { + lastRevocationCheckData = getLastRevocationCheckData(); + } catch (IOException ex2) { + throw new CertPathValidatorException( + "Unable to load stored revocation status", ex2); + } + for (String serialNumber : serialNumbers) { + if (!lastRevocationCheckData.containsKey(serialNumber) + || lastRevocationCheckData + .get(serialNumber) + .isBefore( + LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) { + throw new CertPathValidatorException( + "Unable to verify the revocation status of certificate " + + serialNumber); + } + } + } + } + + private static boolean needToCheckRevocationStatus( + List<X509Certificate> certificatesOrderedLeafFirst) { + if (certificatesOrderedLeafFirst.isEmpty()) { + return false; + } + // A certificate isn't revoked when it is first issued, so we treat it as checked on its + // issue date. + if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) { + return true; + } + for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) { + if (!isIssuedWithinDays( + certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) { + return true; + } + } + return false; + } + + private static boolean isIssuedWithinDays(X509Certificate certificate, int days) { + LocalDate notBeforeDate = + LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()); + LocalDate expectedIssueData = + notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES); + return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData); + } + + void updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + JSONObject revocationList, Collection<String> otherCertificatesToCheck) { + Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck); + try { + allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet()); + } catch (IOException ex) { + Slog.e(TAG, "Unable to update last check date of stored data.", ex); + } + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (String serialNumber : allCertificatesToCheck) { + areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber)); + } + updateLastRevocationCheckData(areCertificatesRevoked); + } + + /** + * Update the last revocation check data stored on this device. + * + * @param areCertificatesRevoked A Map whose keys are certificate serial numbers and values are + * whether that certificate has been revoked + */ + void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) { + LocalDateTime now = LocalDateTime.now(); + synchronized (sFileLock) { + Map<String, LocalDateTime> lastRevocationCheckData; + try { + lastRevocationCheckData = getLastRevocationCheckData(); + } catch (IOException ex) { + Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex); + return; + } + for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) { + if (entry.getValue()) { + lastRevocationCheckData.remove(entry.getKey()); + } else { + lastRevocationCheckData.put(entry.getKey(), now); + } + } + storeLastRevocationCheckData(lastRevocationCheckData); + } + } + + Map<String, LocalDateTime> getLastRevocationCheckData() throws IOException { + Map<String, LocalDateTime> data = new HashMap<>(); + File dataFile = getLastRevocationCheckDataFile(); + synchronized (sFileLock) { + if (!dataFile.exists()) { + return data; + } + String dataString; + try (FileInputStream in = new FileInputStream(dataFile)) { + dataString = new String(in.readAllBytes(), UTF_8); + } + for (String line : dataString.split(System.lineSeparator())) { + String[] elements = line.split(REVOCATION_STATUS_FILE_FIELD_DELIMITER); + if (elements.length != 2) { + continue; + } + try { + data.put(elements[0], LocalDateTime.parse(elements[1])); + } catch (DateTimeParseException ex) { + Slog.e( + TAG, + "Unable to parse last checked LocalDateTime from file. Deleting the" + + " potentially corrupted file.", + ex); + dataFile.delete(); + return data; + } + } + } + return data; + } + + @VisibleForTesting + void storeLastRevocationCheckData(Map<String, LocalDateTime> lastRevocationCheckData) { + StringBuilder dataStringBuilder = new StringBuilder(); + for (Map.Entry<String, LocalDateTime> entry : lastRevocationCheckData.entrySet()) { + dataStringBuilder + .append(entry.getKey()) + .append(REVOCATION_STATUS_FILE_FIELD_DELIMITER) + .append(entry.getValue()) + .append(System.lineSeparator()); + } + synchronized (sFileLock) { + try (FileOutputStream fileOutputStream = + new FileOutputStream(getLastRevocationCheckDataFile())) { + fileOutputStream.write(dataStringBuilder.toString().getBytes(UTF_8)); + Slog.d(TAG, "Successfully stored revocation status data."); + } catch (IOException ex) { + Slog.e(TAG, "Failed to store revocation status data.", ex); + } + } + } + + private File getLastRevocationCheckDataFile() { + if (mTestRevocationStatusFile != null) { + return mTestRevocationStatusFile; + } + return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME); + } + + private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) { + JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Slog.e(TAG, "Unable to get job scheduler."); + return; + } + Slog.d(TAG, "Scheduling job to fetch remote CRL."); + PersistableBundle extras = new PersistableBundle(); + extras.putStringArray( + UpdateCertificateRevocationStatusJobService.EXTRA_KEY_CERTIFICATES_TO_CHECK, + serialNumbers.toArray(new String[0])); + jobScheduler.schedule( + new JobInfo.Builder( + JOB_ID, + new ComponentName( + mContext, + UpdateCertificateRevocationStatusJobService.class)) + .setExtras(extras) + .setRequiredNetwork( + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build()) + .build()); + } + + /** + * Fetches the revocation list from the URL specified in + * R.string.vendor_required_attestation_revocation_list_url + * + * @return The remote revocation list entries in a JSONObject + * @throws CertPathValidatorException if the URL is not defined or is malformed. + * @throws IOException if the URL is valid but the fetch failed. + * @throws JSONException if the revocation list content cannot be parsed + */ + JSONObject fetchRemoteRevocationList() + throws CertPathValidatorException, IOException, JSONException { + String urlString = getRemoteRevocationListUrl(); + if (urlString == null || urlString.isEmpty()) { + throw new CertPathValidatorException( + "R.string.vendor_required_attestation_revocation_list_url is empty."); + } + URL url; + try { + url = new URL(urlString); + } catch (MalformedURLException ex) { + throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex); + } + byte[] revocationListBytes; + try (InputStream inputStream = url.openStream()) { + revocationListBytes = inputStream.readAllBytes(); + } + JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8)); + return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY); + } + + private String getRemoteRevocationListUrl() { + if (mTestRemoteRevocationListUrl != null) { + return mTestRemoteRevocationListUrl; + } + return mContext.getResources() + .getString(R.string.vendor_required_attestation_revocation_list_url); + } +} diff --git a/services/core/java/com/android/server/security/OWNERS b/services/core/java/com/android/server/security/OWNERS index fa4bf228c683..7a31a0006bb9 100644 --- a/services/core/java/com/android/server/security/OWNERS +++ b/services/core/java/com/android/server/security/OWNERS @@ -3,5 +3,6 @@ include /core/java/android/security/OWNERS per-file *AttestationVerification* = file:/core/java/android/security/attestationverification/OWNERS +per-file *CertificateRevocationStatus* = file:/core/java/android/security/attestationverification/OWNERS per-file FileIntegrity*.java = victorhsieh@google.com per-file KeyChainSystemService.java = file:platform/packages/apps/KeyChain:/OWNERS diff --git a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java new file mode 100644 index 000000000000..768c812f47a3 --- /dev/null +++ b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.security; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.util.Slog; + +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** A {@link JobService} that fetches the certificate revocation list from a remote location. */ +public class UpdateCertificateRevocationStatusJobService extends JobService { + + static final String EXTRA_KEY_CERTIFICATES_TO_CHECK = + "com.android.server.security.extra.CERTIFICATES_TO_CHECK"; + private static final String TAG = "AVF_CRL"; + private ExecutorService mExecutorService; + + @Override + public void onCreate() { + super.onCreate(); + mExecutorService = Executors.newSingleThreadExecutor(); + } + + @Override + public boolean onStartJob(JobParameters params) { + mExecutorService.execute( + () -> { + try { + CertificateRevocationStatusManager certificateRevocationStatusManager = + new CertificateRevocationStatusManager(this); + Slog.d(TAG, "Starting to fetch remote CRL from job service."); + JSONObject revocationList = + certificateRevocationStatusManager.fetchRemoteRevocationList(); + String[] certificatesToCheckFromJobParams = + params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK); + if (certificatesToCheckFromJobParams == null) { + Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK); + return; + } + certificateRevocationStatusManager + .updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + revocationList, + Arrays.asList(certificatesToCheckFromJobParams)); + } catch (Throwable t) { + Slog.e(TAG, "Unable to update the stored revocation status.", t); + } + jobFinished(params, false); + }); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mExecutorService.shutdown(); + } +} diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 20917ba21439..cf9c57aa634a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -113,7 +113,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED; import static android.view.WindowManager.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; import static android.view.WindowManager.TRANSIT_OLD_UNSET; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.hasWindowExtensionsEnabled; @@ -247,7 +246,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES; import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; @@ -659,8 +657,6 @@ final class ActivityRecord extends WindowToken { private RemoteAnimationDefinition mRemoteAnimationDefinition; - AnimatingActivityRegistry mAnimatingActivityRegistry; - // Set to the previous Task parent of the ActivityRecord when it is reparented to a new Task // due to picture-in-picture. This gets cleared whenever this activity or the Task // it references to gets removed. This should also be cleared when we move out of pip. @@ -857,12 +853,6 @@ final class ActivityRecord extends WindowToken { }) @interface SplashScreenBehavior { } - // Force an app transition to be ran in the case the visibility of the app did not change. - // We use this for the case of moving a Root Task to the back with multiple activities, and the - // top activity enters PIP; the bottom activity's visibility stays the same, but we need to - // run the transition. - boolean mRequestForceTransition; - boolean mEnteringAnimation; boolean mOverrideTaskTransition; boolean mDismissKeyguardIfInsecure; @@ -1587,9 +1577,6 @@ final class ActivityRecord extends WindowToken { } } final Task rootTask = getRootTask(); - - updateAnimatingActivityRegistry(); - if (task == mLastParentBeforePip && task != null) { // Notify the TaskFragmentOrganizer that the activity is reparented back from pip. mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController @@ -1691,20 +1678,6 @@ final class ActivityRecord extends WindowToken { return !organizedTaskFragment.isAllowedToEmbedActivityInTrustedMode(this); } - void updateAnimatingActivityRegistry() { - final Task rootTask = getRootTask(); - final AnimatingActivityRegistry registry = rootTask != null - ? rootTask.getAnimatingActivityRegistry() - : null; - - // If we reparent, make sure to remove ourselves from the old animation registry. - if (mAnimatingActivityRegistry != null && mAnimatingActivityRegistry != registry) { - mAnimatingActivityRegistry.notifyFinished(this); - } - - mAnimatingActivityRegistry = registry; - } - boolean canAutoEnterPip() { // beforeStopping=false since the actual pip-ing will take place after startPausing() final boolean activityCanPip = checkEnterPictureInPictureState( @@ -1789,7 +1762,6 @@ final class ActivityRecord extends WindowToken { if (prevDc.mOpeningApps.remove(this)) { // Transfer opening transition to new display. mDisplayContent.mOpeningApps.add(this); - mDisplayContent.transferAppTransitionFrom(prevDc); mDisplayContent.executeAppTransition(); } @@ -4632,12 +4604,6 @@ final class ActivityRecord extends WindowToken { } } - // In this case, the starting icon has already been displayed, so start - // letting windows get shown immediately without any more transitions. - if (fromActivity.mVisible) { - mDisplayContent.mSkipAppTransitionAnimation = true; - } - ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Moving existing starting %s" + " from %s to %s", tStartingWindow, fromActivity, this); @@ -5668,76 +5634,17 @@ final class ActivityRecord extends WindowToken { mTransitionController.mValidateCommitVis.add(this); return; } - // If we are preparing an app transition, then delay changing - // the visibility of this token until we execute that transition. - if (deferCommitVisibilityChange(visible)) { - return; - } commitVisibility(visible, true /* performLayout */); updateReportedVisibilityLocked(); } - /** - * Returns {@code true} if this activity is either added to opening-apps or closing-apps. - * Then its visibility will be committed until the transition is ready. - */ - private boolean deferCommitVisibilityChange(boolean visible) { - if (mTransitionController.isShellTransitionsEnabled()) { - // Shell transition doesn't use opening/closing sets. - return false; - } - if (!mDisplayContent.mAppTransition.isTransitionSet()) { - return false; - } - if (mWaitForEnteringPinnedMode && mVisible == visible) { - // If the visibility is not changed during enter PIP, we don't want to include it in - // app transition to affect the animation theme, because the Pip organizer will - // animate the entering PIP instead. - return false; - } - - // The animation will be visible soon so do not skip by screen off. - final boolean ignoreScreenOn = canTurnScreenOn() || mTaskSupervisor.getKeyguardController() - .isKeyguardGoingAway(mDisplayContent.mDisplayId); - // Ignore display frozen so the opening / closing transition type can be updated correctly - // even if the display is frozen. And it's safe since in applyAnimation will still check - // DC#okToAnimate again if the transition animation is fine to apply. - if (!okToAnimate(true /* ignoreFrozen */, ignoreScreenOn)) { - return false; - } - if (visible) { - mDisplayContent.mOpeningApps.add(this); - mEnteringAnimation = true; - } else if (mVisible) { - mDisplayContent.mClosingApps.add(this); - mEnteringAnimation = false; - } - if ((mDisplayContent.mAppTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0) { - // Add the launching-behind activity to mOpeningApps. - final WindowState win = mDisplayContent.findFocusedWindow(); - if (win != null) { - final ActivityRecord focusedActivity = win.mActivityRecord; - if (focusedActivity != null) { - ProtoLog.d(WM_DEBUG_APP_TRANSITIONS, - "TRANSIT_FLAG_OPEN_BEHIND, adding %s to mOpeningApps", - focusedActivity); - // Force animation to be loaded. - mDisplayContent.mOpeningApps.add(focusedActivity); - } - } - } - return true; - } - @Override boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter, boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) { if ((mTransitionChangeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { return false; } - // If it was set to true, reset the last request to force the transition. - mRequestForceTransition = false; return super.applyAnimation(lp, transit, enter, isVoiceInteraction, sources); } @@ -5900,27 +5807,6 @@ final class ActivityRecord extends WindowToken { } /** - * Check if visibility of this {@link ActivityRecord} should be updated as part of an app - * transition. - * - * <p class="note><strong>Note:</strong> If the visibility of this {@link ActivityRecord} is - * already set to {@link #mVisible}, we don't need to update the visibility. So {@code false} is - * returned.</p> - * - * @param visible {@code true} if this {@link ActivityRecord} should become visible, - * {@code false} if this should become invisible. - * @return {@code true} if visibility of this {@link ActivityRecord} should be updated, and - * an app transition animation should be run. - */ - boolean shouldApplyAnimation(boolean visible) { - // Allow for state update and animation to be applied if: - // * activity is transitioning visibility state - // * or the activity was marked as hidden and is exiting before we had a chance to play the - // transition animation - return isVisible() != visible || mRequestForceTransition || (!isVisible() && mIsExiting); - } - - /** * See {@link Activity#setRecentsScreenshotEnabled}. */ void setRecentsScreenshotEnabled(boolean enabled) { @@ -6208,13 +6094,8 @@ final class ActivityRecord extends WindowToken { return false; } - // Hide all activities on the presenting display so that malicious apps can't do tap - // jacking (b/391466268). - // For now, this should only be applied to external displays because presentations can only - // be shown on them. - // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that - // the presentation won't stop its controlling activity. - if (enablePresentationForConnectedDisplays() && mDisplayContent.mIsPresenting) { + // A presentation stopps all activities behind on the same display. + if (mWmService.mPresentationController.shouldOccludeActivities(getDisplayId())) { return false; } @@ -7635,13 +7516,6 @@ final class ActivityRecord extends WindowToken { } @Override - public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { - return mAnimatingActivityRegistry != null - && mAnimatingActivityRegistry.notifyAboutToFinish( - this, endDeferFinishCallback); - } - - @Override boolean isWaitingForTransitionStart() { final DisplayContent dc = getDisplayContent(); return dc != null && dc.mAppTransition.isTransitionSet() @@ -7662,10 +7536,6 @@ final class ActivityRecord extends WindowToken { @Override public void onLeashAnimationStarting(Transaction t, SurfaceControl leash) { - if (mAnimatingActivityRegistry != null) { - mAnimatingActivityRegistry.notifyStarting(this); - } - if (mNeedsLetterboxedAnimation) { updateLetterboxSurfaceIfNeeded(findMainWindow(), t); mNeedsAnimationBoundsLayer = true; @@ -7676,17 +7546,7 @@ final class ActivityRecord extends WindowToken { // new layer. if (mNeedsAnimationBoundsLayer) { mTmpRect.setEmpty(); - if (getDisplayContent().mAppTransitionController.isTransitWithinTask( - getTransit(), task)) { - task.getBounds(mTmpRect); - } else { - final Task rootTask = getRootTask(); - if (rootTask == null) { - return; - } - // Set clip rect to root task bounds. - rootTask.getBounds(mTmpRect); - } + task.getBounds(mTmpRect); mAnimationBoundsLayer = createAnimationBoundsLayer(t); // Crop to root task bounds. @@ -7842,10 +7702,6 @@ final class ActivityRecord extends WindowToken { mNeedsLetterboxedAnimation = false; updateLetterboxSurfaceIfNeeded(findMainWindow(), t); } - - if (mAnimatingActivityRegistry != null) { - mAnimatingActivityRegistry.notifyFinished(this); - } } @Override diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 254127dee7a8..819e117e6d05 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4128,22 +4128,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Override public void registerRemoteAnimationsForDisplay(int displayId, RemoteAnimationDefinition definition) { - mAmInternal.enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, - "registerRemoteAnimations"); - definition.setCallingPidUid(Binder.getCallingPid(), Binder.getCallingUid()); - synchronized (mGlobalLock) { - final DisplayContent display = mRootWindowContainer.getDisplayContent(displayId); - if (display == null) { - Slog.e(TAG, "Couldn't find display with id: " + displayId); - return; - } - final long origId = Binder.clearCallingIdentity(); - try { - display.registerRemoteAnimations(definition); - } finally { - Binder.restoreCallingIdentity(origId); - } - } + // TODO(b/365884835): Remove callers. } /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */ diff --git a/services/core/java/com/android/server/wm/AnimatingActivityRegistry.java b/services/core/java/com/android/server/wm/AnimatingActivityRegistry.java deleted file mode 100644 index 18ec96c38264..000000000000 --- a/services/core/java/com/android/server/wm/AnimatingActivityRegistry.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.wm; - -import android.util.ArrayMap; -import android.util.ArraySet; - -import java.io.PrintWriter; -import java.util.ArrayList; - -/** - * Keeps track of all {@link ActivityRecord} that are animating and makes sure all animations are - * finished at the same time such that we don't run into issues with z-ordering: An activity A - * that has a shorter animation that is above another activity B with a longer animation in the same - * task, the animation layer would put the B on top of A, but from the hierarchy, A needs to be on - * top of B. Thus, we defer reparenting A to the original hierarchy such that it stays on top of B - * until B finishes animating. - */ -class AnimatingActivityRegistry { - - private ArraySet<ActivityRecord> mAnimatingActivities = new ArraySet<>(); - private ArrayMap<ActivityRecord, Runnable> mFinishedTokens = new ArrayMap<>(); - - private ArrayList<Runnable> mTmpRunnableList = new ArrayList<>(); - - private boolean mEndingDeferredFinish; - - /** - * Notifies that an {@link ActivityRecord} has started animating. - */ - void notifyStarting(ActivityRecord token) { - mAnimatingActivities.add(token); - } - - /** - * Notifies that an {@link ActivityRecord} has finished animating. - */ - void notifyFinished(ActivityRecord activity) { - mAnimatingActivities.remove(activity); - mFinishedTokens.remove(activity); - - // If we were the last activity, make sure the end all deferred finishes. - if (mAnimatingActivities.isEmpty()) { - endDeferringFinished(); - } - } - - /** - * Called when an {@link ActivityRecord} is about to finish animating. - * - * @param endDeferFinishCallback Callback to run when defer finish should be ended. - * @return {@code true} if finishing the animation should be deferred, {@code false} otherwise. - */ - boolean notifyAboutToFinish(ActivityRecord activity, Runnable endDeferFinishCallback) { - final boolean removed = mAnimatingActivities.remove(activity); - if (!removed) { - return false; - } - - if (mAnimatingActivities.isEmpty()) { - - // If no animations are animating anymore, finish all others. - endDeferringFinished(); - return false; - } else { - - // Otherwise let's put it into the pending list of to be finished animations. - mFinishedTokens.put(activity, endDeferFinishCallback); - return true; - } - } - - private void endDeferringFinished() { - - // Don't start recursing. Running the finished listener invokes notifyFinished, which may - // invoked us again. - if (mEndingDeferredFinish) { - return; - } - try { - mEndingDeferredFinish = true; - - // Copy it into a separate temp list to avoid modifying the collection while iterating - // as calling the callback may call back into notifyFinished. - for (int i = mFinishedTokens.size() - 1; i >= 0; i--) { - mTmpRunnableList.add(mFinishedTokens.valueAt(i)); - } - mFinishedTokens.clear(); - for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) { - mTmpRunnableList.get(i).run(); - } - mTmpRunnableList.clear(); - } finally { - mEndingDeferredFinish = false; - } - } - - void dump(PrintWriter pw, String header, String prefix) { - if (!mAnimatingActivities.isEmpty() || !mFinishedTokens.isEmpty()) { - pw.print(prefix); pw.println(header); - prefix = prefix + " "; - pw.print(prefix); pw.print("mAnimatingActivities="); pw.println(mAnimatingActivities); - pw.print(prefix); pw.print("mFinishedTokens="); pw.println(mFinishedTokens); - } - } -} diff --git a/services/core/java/com/android/server/wm/AnimationAdapter.java b/services/core/java/com/android/server/wm/AnimationAdapter.java index 3dc377dbc14c..4458ed76dcba 100644 --- a/services/core/java/com/android/server/wm/AnimationAdapter.java +++ b/services/core/java/com/android/server/wm/AnimationAdapter.java @@ -109,15 +109,6 @@ public interface AnimationAdapter { * Gets called when the animation is about to finish and gives the client the opportunity to * defer finishing the animation, i.e. it keeps the leash around until the client calls * endDeferFinishCallback. - * <p> - * This has the same effect as - * {@link com.android.server.wm.SurfaceAnimator.Animatable#shouldDeferAnimationFinish(Runnable)} - * . The later will be evaluated first and has precedence over this method if it returns true, - * which means that if the {@link com.android.server.wm.SurfaceAnimator.Animatable} requests to - * defer its finish, this method won't be called so this adapter will never have access to the - * finish callback. On the other hand, if the - * {@link com.android.server.wm.SurfaceAnimator.Animatable}, doesn't request to defer, this - * {@link AnimationAdapter} is responsible for ending the animation. * * @param endDeferFinishCallback The callback to call when defer finishing should be ended. * @return Whether the client would like to defer the animation finish. diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index 932f26857105..9c4b722feb47 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -1462,7 +1462,7 @@ public class AppTransition implements Dump { } boolean prepareAppTransition(@TransitionType int transit, @TransitionFlags int flags) { - if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) { + if (WindowManagerService.sEnableShellTransitions) { return false; } mNextAppTransitionRequests.add(transit); diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java deleted file mode 100644 index d5fe056a2ba4..000000000000 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ /dev/null @@ -1,1352 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.wm; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; -import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; -import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_RELAUNCH; -import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_OLD_NONE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND; -import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK; -import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_FRONT; -import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_OPEN; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_RELAUNCH; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; - -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT; -import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SNAPSHOT; -import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN; -import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN; -import static com.android.server.wm.AppTransition.isNormalTransit; -import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldAttachNavBarToApp; -import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldStartNonAppWindowAnimationsForKeyguardExit; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation; -import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; - -import android.annotation.IntDef; -import android.annotation.Nullable; -import android.graphics.Rect; -import android.os.Trace; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.Pair; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationDefinition; -import android.view.WindowManager; -import android.view.WindowManager.LayoutParams; -import android.view.WindowManager.TransitionFlags; -import android.view.WindowManager.TransitionOldType; -import android.view.WindowManager.TransitionType; -import android.window.ITaskFragmentOrganizer; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLog; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.function.Consumer; -import java.util.function.Predicate; - -/** - * Checks for app transition readiness, resolves animation attributes and performs visibility - * change for apps that animate as part of an app transition. - */ -public class AppTransitionController { - private static final String TAG = TAG_WITH_CLASS_NAME ? "AppTransitionController" : TAG_WM; - private final WindowManagerService mService; - private final DisplayContent mDisplayContent; - private final WallpaperController mWallpaperControllerLocked; - private RemoteAnimationDefinition mRemoteAnimationDefinition = null; - - private static final int TYPE_NONE = 0; - private static final int TYPE_ACTIVITY = 1; - private static final int TYPE_TASK_FRAGMENT = 2; - private static final int TYPE_TASK = 3; - - @IntDef(prefix = { "TYPE_" }, value = { - TYPE_NONE, - TYPE_ACTIVITY, - TYPE_TASK_FRAGMENT, - TYPE_TASK - }) - @Retention(RetentionPolicy.SOURCE) - @interface TransitContainerType {} - - private final ArrayMap<WindowContainer, Integer> mTempTransitionReasons = new ArrayMap<>(); - private final ArrayList<WindowContainer> mTempTransitionWindows = new ArrayList<>(); - - AppTransitionController(WindowManagerService service, DisplayContent displayContent) { - mService = service; - mDisplayContent = displayContent; - mWallpaperControllerLocked = mDisplayContent.mWallpaperController; - } - - void registerRemoteAnimations(RemoteAnimationDefinition definition) { - mRemoteAnimationDefinition = definition; - } - - /** - * Returns the currently visible window that is associated with the wallpaper in case we are - * transitioning from an activity with a wallpaper to one without. - */ - @Nullable - private WindowState getOldWallpaper() { - final WindowState wallpaperTarget = mWallpaperControllerLocked.getWallpaperTarget(); - final @TransitionType int firstTransit = - mDisplayContent.mAppTransition.getFirstAppTransition(); - - final ArraySet<WindowContainer> openingWcs = getAnimationTargets( - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, true /* visible */); - final boolean showWallpaper = wallpaperTarget != null - && (wallpaperTarget.hasWallpaper() - // Update task open transition to wallpaper transition when wallpaper is visible. - // (i.e.launching app info activity from recent tasks) - || ((firstTransit == TRANSIT_OPEN || firstTransit == TRANSIT_TO_FRONT) - && (!openingWcs.isEmpty() && openingWcs.valueAt(0).asTask() != null) - && mWallpaperControllerLocked.isWallpaperVisible())); - // If wallpaper is animating or wallpaperTarget doesn't have SHOW_WALLPAPER flag set, - // don't consider upgrading to wallpaper transition. - return (mWallpaperControllerLocked.isWallpaperTargetAnimating() || !showWallpaper) - ? null : wallpaperTarget; - } - - /** - * Handle application transition for given display. - */ - void handleAppTransitionReady() { - mTempTransitionReasons.clear(); - if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons) - || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons) - || !transitionGoodToGoForTaskFragments()) { - return; - } - - Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady"); - - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "**** GOOD TO GO"); - // TODO(b/205335975): Remove window which stuck in animatingExit status. Find actual cause. - mDisplayContent.forAllWindows(WindowState::cleanupAnimatingExitWindow, - true /* traverseTopToBottom */); - // TODO(new-app-transition): Remove code using appTransition.getAppTransition() - final AppTransition appTransition = mDisplayContent.mAppTransition; - - mDisplayContent.mNoAnimationNotifyOnTransitionFinished.clear(); - - appTransition.removeAppTransitionTimeoutCallbacks(); - - mDisplayContent.mWallpaperMayChange = false; - - int appCount = mDisplayContent.mOpeningApps.size(); - for (int i = 0; i < appCount; ++i) { - // Clearing the mAnimatingExit flag before entering animation. It's set to true if app - // window is removed, or window relayout to invisible. This also affects window - // visibility. We need to clear it *before* maybeUpdateTransitToWallpaper() as the - // transition selection depends on wallpaper target visibility. - mDisplayContent.mOpeningApps.valueAtUnchecked(i).clearAnimatingFlags(); - } - appCount = mDisplayContent.mChangingContainers.size(); - for (int i = 0; i < appCount; ++i) { - // Clearing for same reason as above. - final ActivityRecord activity = getAppFromContainer( - mDisplayContent.mChangingContainers.valueAtUnchecked(i)); - if (activity != null) { - activity.clearAnimatingFlags(); - } - } - - // Adjust wallpaper before we pull the lower/upper target, since pending changes - // (like the clearAnimatingFlags() above) might affect wallpaper target result. - // Or, the opening app window should be a wallpaper target. - mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded( - mDisplayContent.mOpeningApps); - - ArraySet<ActivityRecord> tmpOpenApps = mDisplayContent.mOpeningApps; - ArraySet<ActivityRecord> tmpCloseApps = mDisplayContent.mClosingApps; - if (mDisplayContent.mAtmService.mBackNavigationController.isMonitoringFinishTransition()) { - tmpOpenApps = new ArraySet<>(mDisplayContent.mOpeningApps); - tmpCloseApps = new ArraySet<>(mDisplayContent.mClosingApps); - } - - @TransitionOldType final int transit = getTransitCompatType( - mDisplayContent.mAppTransition, tmpOpenApps, - tmpCloseApps, mDisplayContent.mChangingContainers, - mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(), - mDisplayContent.mSkipAppTransitionAnimation); - mDisplayContent.mSkipAppTransitionAnimation = false; - - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "handleAppTransitionReady: displayId=%d appTransition={%s}" - + " openingApps=[%s] closingApps=[%s] transit=%s", - mDisplayContent.mDisplayId, appTransition.toString(), tmpOpenApps, - tmpCloseApps, AppTransition.appTransitionOldToString(transit)); - - // Find the layout params of the top-most application window in the tokens, which is - // what will control the animation theme. If all closing windows are obscured, then there is - // no need to do an animation. This is the case, for example, when this transition is being - // done behind a dream window. - final ArraySet<Integer> activityTypes = collectActivityTypes(tmpOpenApps, - tmpCloseApps, mDisplayContent.mChangingContainers); - final ActivityRecord animLpActivity = findAnimLayoutParamsToken(transit, activityTypes, - tmpOpenApps, tmpCloseApps, mDisplayContent.mChangingContainers); - final ActivityRecord topOpeningApp = - getTopApp(tmpOpenApps, false /* ignoreHidden */); - final ActivityRecord topClosingApp = - getTopApp(tmpCloseApps, false /* ignoreHidden */); - final ActivityRecord topChangingApp = - getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */); - final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity); - - // Check if there is any override - if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) { - // Unfreeze the windows that were previously frozen for TaskFragment animation. - overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes); - } - - final boolean voiceInteraction = containsVoiceInteraction(mDisplayContent.mClosingApps) - || containsVoiceInteraction(mDisplayContent.mOpeningApps); - - final int layoutRedo; - mService.mSurfaceAnimationRunner.deferStartingAnimations(); - try { - applyAnimations(tmpOpenApps, tmpCloseApps, transit, animLp, voiceInteraction); - handleClosingApps(); - handleOpeningApps(); - handleChangingApps(transit); - handleClosingChangingContainers(); - - appTransition.setLastAppTransition(transit, topOpeningApp, - topClosingApp, topChangingApp); - - final int flags = appTransition.getTransitFlags(); - layoutRedo = appTransition.goodToGo(transit, topOpeningApp); - appTransition.postAnimationCallback(); - } finally { - appTransition.clear(); - mService.mSurfaceAnimationRunner.continueStartingAnimations(); - } - - mService.mSnapshotController.onTransitionStarting(mDisplayContent); - - mDisplayContent.mOpeningApps.clear(); - mDisplayContent.mClosingApps.clear(); - mDisplayContent.mChangingContainers.clear(); - mDisplayContent.mUnknownAppVisibilityController.clear(); - mDisplayContent.mClosingChangingContainers.clear(); - - // This has changed the visibility of windows, so perform - // a new layout to get them all up-to-date. - mDisplayContent.setLayoutNeeded(); - - mDisplayContent.computeImeTarget(true /* updateImeTarget */); - - mService.mAtmService.mTaskSupervisor.getActivityMetricsLogger().notifyTransitionStarting( - mTempTransitionReasons); - - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - - mDisplayContent.pendingLayoutChanges |= - layoutRedo | FINISH_LAYOUT_REDO_LAYOUT | FINISH_LAYOUT_REDO_CONFIG; - } - - /** - * Get old transit type based on the current transit requests. - * - * @param appTransition {@link AppTransition} for managing app transition state. - * @param openingApps {@link ActivityRecord}s which are becoming visible. - * @param closingApps {@link ActivityRecord}s which are becoming invisible. - * @param changingContainers {@link WindowContainer}s which are changed in configuration. - * @param wallpaperTarget If non-null, this is the currently visible window that is associated - * with the wallpaper. - * @param oldWallpaper The currently visible window that is associated with the wallpaper in - * case we are transitioning from an activity with a wallpaper to one - * without. Otherwise null. - */ - @TransitionOldType static int getTransitCompatType(AppTransition appTransition, - ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps, - ArraySet<WindowContainer> changingContainers, @Nullable WindowState wallpaperTarget, - @Nullable WindowState oldWallpaper, boolean skipAppTransitionAnimation) { - - final ActivityRecord topOpeningApp = getTopApp(openingApps, false /* ignoreHidden */); - final ActivityRecord topClosingApp = getTopApp(closingApps, true /* ignoreHidden */); - - // Determine if closing and opening app token sets are wallpaper targets, in which case - // special animations are needed. - final boolean openingAppHasWallpaper = canBeWallpaperTarget(openingApps) - && wallpaperTarget != null; - final boolean closingAppHasWallpaper = canBeWallpaperTarget(closingApps) - && wallpaperTarget != null; - - // Keyguard transit has high priority. - switch (appTransition.getKeyguardTransition()) { - case TRANSIT_KEYGUARD_GOING_AWAY: - return openingAppHasWallpaper ? TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER - : TRANSIT_OLD_KEYGUARD_GOING_AWAY; - case TRANSIT_KEYGUARD_OCCLUDE: - // When there is a closing app, the keyguard has already been occluded by an - // activity, and another activity has started on top of that activity, so normal - // app transition animation should be used. - if (!closingApps.isEmpty()) { - return TRANSIT_OLD_ACTIVITY_OPEN; - } - if (!openingApps.isEmpty() && openingApps.valueAt(0).getActivityType() - == ACTIVITY_TYPE_DREAM) { - return TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM; - } - return TRANSIT_OLD_KEYGUARD_OCCLUDE; - case TRANSIT_KEYGUARD_UNOCCLUDE: - return TRANSIT_OLD_KEYGUARD_UNOCCLUDE; - } - - // Determine whether the top opening and closing activity is a dream activity. If so, this - // has higher priority than others except keyguard transit. - if (topOpeningApp != null && topOpeningApp.getActivityType() == ACTIVITY_TYPE_DREAM) { - return TRANSIT_OLD_DREAM_ACTIVITY_OPEN; - } else if (topClosingApp != null - && topClosingApp.getActivityType() == ACTIVITY_TYPE_DREAM) { - return TRANSIT_OLD_DREAM_ACTIVITY_CLOSE; - } - - // This is not keyguard transition and one of the app has request to skip app transition. - if (skipAppTransitionAnimation) { - return WindowManager.TRANSIT_OLD_UNSET; - } - @TransitionFlags final int flags = appTransition.getTransitFlags(); - @TransitionType final int firstTransit = appTransition.getFirstAppTransition(); - - // Special transitions - // TODO(new-app-transitions): Revisit if those can be rewritten by using flags. - if (appTransition.containsTransitRequest(TRANSIT_CHANGE) && !changingContainers.isEmpty()) { - @TransitContainerType int changingType = - getTransitContainerType(changingContainers.valueAt(0)); - switch (changingType) { - case TYPE_TASK: - return TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; - case TYPE_TASK_FRAGMENT: - return TRANSIT_OLD_TASK_FRAGMENT_CHANGE; - default: - throw new IllegalStateException( - "TRANSIT_CHANGE with unrecognized changing type=" + changingType); - } - } - if ((flags & TRANSIT_FLAG_APP_CRASHED) != 0) { - return TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE; - } - if (firstTransit == TRANSIT_NONE) { - return TRANSIT_OLD_NONE; - } - - /* - * There are cases where we open/close a new task/activity, but in reality only a - * translucent activity on top of existing activities is opening/closing. For that one, we - * have a different animation because non of the task/activity animations actually work well - * with translucent apps. - */ - if (isNormalTransit(firstTransit)) { - boolean allOpeningVisible = true; - boolean allTranslucentOpeningApps = !openingApps.isEmpty(); - for (int i = openingApps.size() - 1; i >= 0; i--) { - final ActivityRecord activity = openingApps.valueAt(i); - if (!activity.isVisible()) { - allOpeningVisible = false; - if (activity.fillsParent()) { - allTranslucentOpeningApps = false; - } - } - } - boolean allTranslucentClosingApps = !closingApps.isEmpty(); - for (int i = closingApps.size() - 1; i >= 0; i--) { - if (closingApps.valueAt(i).fillsParent()) { - allTranslucentClosingApps = false; - break; - } - } - - if (allTranslucentClosingApps && allOpeningVisible) { - return TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE; - } - if (allTranslucentOpeningApps && closingApps.isEmpty()) { - return TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN; - } - } - - if (closingAppHasWallpaper && openingAppHasWallpaper) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Wallpaper animation!"); - switch (firstTransit) { - case TRANSIT_OPEN: - case TRANSIT_TO_FRONT: - return TRANSIT_OLD_WALLPAPER_INTRA_OPEN; - case TRANSIT_CLOSE: - case TRANSIT_TO_BACK: - return TRANSIT_OLD_WALLPAPER_INTRA_CLOSE; - } - } else if (oldWallpaper != null && !openingApps.isEmpty() - && !openingApps.contains(oldWallpaper.mActivityRecord) - && closingApps.contains(oldWallpaper.mActivityRecord) - && topClosingApp == oldWallpaper.mActivityRecord) { - // We are transitioning from an activity with a wallpaper to one without. - return TRANSIT_OLD_WALLPAPER_CLOSE; - } else if (wallpaperTarget != null && wallpaperTarget.isVisible() - && openingApps.contains(wallpaperTarget.mActivityRecord) - && topOpeningApp == wallpaperTarget.mActivityRecord - /* && transit != TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE */) { - // We are transitioning from an activity without - // a wallpaper to now showing the wallpaper - return TRANSIT_OLD_WALLPAPER_OPEN; - } - - final ArraySet<WindowContainer> openingWcs = getAnimationTargets( - openingApps, closingApps, true /* visible */); - final ArraySet<WindowContainer> closingWcs = getAnimationTargets( - openingApps, closingApps, false /* visible */); - final WindowContainer<?> openingContainer = !openingWcs.isEmpty() - ? openingWcs.valueAt(0) : null; - final WindowContainer<?> closingContainer = !closingWcs.isEmpty() - ? closingWcs.valueAt(0) : null; - @TransitContainerType int openingType = getTransitContainerType(openingContainer); - @TransitContainerType int closingType = getTransitContainerType(closingContainer); - if (appTransition.containsTransitRequest(TRANSIT_TO_FRONT) && openingType == TYPE_TASK) { - if (topOpeningApp != null && topOpeningApp.isActivityTypeHome()) { - // If we are opening the home task, we want to play an animation as if - // the task on top is being brought to back. - return TRANSIT_OLD_TASK_TO_BACK; - } - return TRANSIT_OLD_TASK_TO_FRONT; - } - if (appTransition.containsTransitRequest(TRANSIT_TO_BACK) && closingType == TYPE_TASK) { - return TRANSIT_OLD_TASK_TO_BACK; - } - if (appTransition.containsTransitRequest(TRANSIT_OPEN)) { - if (openingType == TYPE_TASK) { - return (appTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0 - ? TRANSIT_OLD_TASK_OPEN_BEHIND : TRANSIT_OLD_TASK_OPEN; - } - if (openingType == TYPE_ACTIVITY) { - return TRANSIT_OLD_ACTIVITY_OPEN; - } - if (openingType == TYPE_TASK_FRAGMENT) { - return TRANSIT_OLD_TASK_FRAGMENT_OPEN; - } - } - if (appTransition.containsTransitRequest(TRANSIT_CLOSE)) { - if (closingType == TYPE_TASK) { - return TRANSIT_OLD_TASK_CLOSE; - } - if (closingType == TYPE_TASK_FRAGMENT) { - return TRANSIT_OLD_TASK_FRAGMENT_CLOSE; - } - if (closingType == TYPE_ACTIVITY) { - for (int i = closingApps.size() - 1; i >= 0; i--) { - if (closingApps.valueAt(i).visibleIgnoringKeyguard) { - return TRANSIT_OLD_ACTIVITY_CLOSE; - } - } - // Skip close activity transition since no closing app can be visible - return WindowManager.TRANSIT_OLD_UNSET; - } - } - if (appTransition.containsTransitRequest(TRANSIT_RELAUNCH) - && !openingWcs.isEmpty() && !openingApps.isEmpty()) { - return TRANSIT_OLD_ACTIVITY_RELAUNCH; - } - return TRANSIT_OLD_NONE; - } - - @TransitContainerType - private static int getTransitContainerType(@Nullable WindowContainer<?> container) { - if (container == null) { - return TYPE_NONE; - } - if (container.asTask() != null) { - return TYPE_TASK; - } - if (container.asTaskFragment() != null) { - return TYPE_TASK_FRAGMENT; - } - if (container.asActivityRecord() != null) { - return TYPE_ACTIVITY; - } - return TYPE_NONE; - } - - @Nullable - private static WindowManager.LayoutParams getAnimLp(ActivityRecord activity) { - final WindowState mainWindow = activity != null ? activity.findMainWindow() : null; - return mainWindow != null ? mainWindow.mAttrs : null; - } - - RemoteAnimationAdapter getRemoteAnimationOverride(@Nullable WindowContainer container, - @TransitionOldType int transit, ArraySet<Integer> activityTypes) { - if (container != null) { - final RemoteAnimationDefinition definition = container.getRemoteAnimationDefinition(); - if (definition != null) { - final RemoteAnimationAdapter adapter = definition.getAdapter(transit, - activityTypes); - if (adapter != null) { - return adapter; - } - } - } - return mRemoteAnimationDefinition != null - ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes) - : null; - } - - private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) { - // We don't want to have the client to animate any non-app windows. - // Having {@code transit} of those types doesn't mean it will contain non-app windows, but - // non-app windows will only be included with those transition types. And we don't currently - // have any use case of those for TaskFragment transition. - return shouldStartNonAppWindowAnimationsForKeyguardExit(transit) - || shouldAttachNavBarToApp(mService, mDisplayContent, transit) - || shouldStartWallpaperAnimation(mDisplayContent); - } - - /** - * Whether the transition contains any embedded {@link TaskFragment} that does not fill the - * parent {@link Task} before or after the transition. - */ - private boolean transitionContainsTaskFragmentWithBoundsOverride() { - for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) { - final WindowContainer wc = mDisplayContent.mChangingContainers.valueAt(i); - if (wc.isEmbedded()) { - // Contains embedded TaskFragment with bounds changed. - return true; - } - } - mTempTransitionWindows.clear(); - mTempTransitionWindows.addAll(mDisplayContent.mClosingApps); - mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps); - boolean containsTaskFragmentWithBoundsOverride = false; - for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) { - final ActivityRecord r = mTempTransitionWindows.get(i).asActivityRecord(); - final TaskFragment tf = r.getTaskFragment(); - if (tf != null && tf.isEmbeddedWithBoundsOverride()) { - containsTaskFragmentWithBoundsOverride = true; - break; - } - } - mTempTransitionWindows.clear(); - return containsTaskFragmentWithBoundsOverride; - } - - /** - * Finds the common parent {@link Task} that is parent of all embedded app windows in the - * current transition. - * @return {@code null} if app windows in the transition are not children of the same Task, or - * if none of the app windows is embedded. - */ - @Nullable - private Task findParentTaskForAllEmbeddedWindows() { - mTempTransitionWindows.clear(); - mTempTransitionWindows.addAll(mDisplayContent.mClosingApps); - mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps); - mTempTransitionWindows.addAll(mDisplayContent.mChangingContainers); - - // It should only animated by the organizer if all windows are below the same leaf Task. - Task leafTask = null; - for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) { - final ActivityRecord r = getAppFromContainer(mTempTransitionWindows.get(i)); - if (r == null) { - leafTask = null; - break; - } - // There are also cases where the Task contains non-embedded activity, such as launching - // split TaskFragments from a non-embedded activity. - // The hierarchy may looks like this: - // - Task - // - Activity - // - TaskFragment - // - Activity - // - TaskFragment - // - Activity - // We also want to have the organizer handle the transition for such case. - final Task task = r.getTask(); - // We don't support embedding in PiP, leave the animation to the PipTaskOrganizer. - if (task == null || task.inPinnedWindowingMode()) { - leafTask = null; - break; - } - // We don't want the organizer to handle transition of other non-embedded Task. - if (leafTask != null && leafTask != task) { - leafTask = null; - break; - } - final ActivityRecord rootActivity = task.getRootActivity(); - // We don't want the organizer to handle transition when the whole app is closing. - if (rootActivity == null) { - leafTask = null; - break; - } - // We don't want the organizer to handle transition of non-embedded activity of other - // app. - if (r.getUid() != task.effectiveUid && !r.isEmbedded()) { - leafTask = null; - break; - } - leafTask = task; - } - mTempTransitionWindows.clear(); - return leafTask; - } - - /** - * Finds the common {@link android.window.TaskFragmentOrganizer} that organizes all embedded - * {@link TaskFragment} belong to the given {@link Task}. - * @return {@code null} if there is no such organizer, or if there are more than one. - */ - @Nullable - private ITaskFragmentOrganizer findTaskFragmentOrganizer(@Nullable Task task) { - if (task == null) { - return null; - } - // We don't support remote animation for Task with multiple TaskFragmentOrganizers. - final ITaskFragmentOrganizer[] organizer = new ITaskFragmentOrganizer[1]; - final boolean hasMultipleOrganizers = task.forAllLeafTaskFragments(taskFragment -> { - final ITaskFragmentOrganizer tfOrganizer = taskFragment.getTaskFragmentOrganizer(); - if (tfOrganizer == null) { - return false; - } - if (organizer[0] != null && !organizer[0].asBinder().equals(tfOrganizer.asBinder())) { - return true; - } - organizer[0] = tfOrganizer; - return false; - }); - if (hasMultipleOrganizers) { - ProtoLog.e(WM_DEBUG_APP_TRANSITIONS, "We don't support remote animation for" - + " Task with multiple TaskFragmentOrganizers."); - return null; - } - return organizer[0]; - } - - /** - * Overrides the pending transition with the remote animation defined by the - * {@link ITaskFragmentOrganizer} if all windows in the transition are children of - * {@link TaskFragment} that are organized by the same organizer. - * - * @return {@code true} if the transition is overridden. - */ - private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit, - ArraySet<Integer> activityTypes) { - if (transitionMayContainNonAppWindows(transit)) { - return false; - } - if (!transitionContainsTaskFragmentWithBoundsOverride()) { - // No need to play TaskFragment remote animation if all embedded TaskFragment in the - // transition fill the Task. - return false; - } - - final Task task = findParentTaskForAllEmbeddedWindows(); - final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizer(task); - final RemoteAnimationDefinition definition = organizer != null - ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController - .getRemoteAnimationDefinition(organizer) - : null; - final RemoteAnimationAdapter adapter = definition != null - ? definition.getAdapter(transit, activityTypes) - : null; - if (adapter == null) { - return false; - } - mDisplayContent.mAppTransition.overridePendingAppTransitionRemote( - adapter, false /* sync */, true /*isActivityEmbedding*/); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "Override with TaskFragment remote animation for transit=%s", - AppTransition.appTransitionOldToString(transit)); - - final int organizerUid = mDisplayContent.mAtmService.mTaskFragmentOrganizerController - .getTaskFragmentOrganizerUid(organizer); - final boolean shouldDisableInputForRemoteAnimation = !task.isFullyTrustedEmbedding( - organizerUid); - final RemoteAnimationController remoteAnimationController = - mDisplayContent.mAppTransition.getRemoteAnimationController(); - if (shouldDisableInputForRemoteAnimation && remoteAnimationController != null) { - // We are going to use client-driven animation, Disable all input on activity windows - // during the animation (unless it is fully trusted) to ensure it is safe to allow - // client to animate the surfaces. - // This is needed for all activity windows in the animation Task. - remoteAnimationController.setOnRemoteAnimationReady(() -> { - final Consumer<ActivityRecord> updateActivities = - activity -> activity.setDropInputForAnimation(true); - task.forAllActivities(updateActivities); - }); - ProtoLog.d(WM_DEBUG_APP_TRANSITIONS, "Task=%d contains embedded TaskFragment." - + " Disabled all input during TaskFragment remote animation.", task.mTaskId); - } - return true; - } - - /** - * Overrides the pending transition with the remote animation defined for the transition in the - * set of defined remote animations in the app window token. - */ - private void overrideWithRemoteAnimationIfSet(@Nullable ActivityRecord animLpActivity, - @TransitionOldType int transit, ArraySet<Integer> activityTypes) { - RemoteAnimationAdapter adapter = null; - if (transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE) { - // The crash transition has higher priority than any involved remote animations. - } else if (AppTransition.isKeyguardGoingAwayTransitOld(transit)) { - adapter = mRemoteAnimationDefinition != null - ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes) - : null; - } else if (mDisplayContent.mAppTransition.getRemoteAnimationController() == null) { - adapter = getRemoteAnimationOverride(animLpActivity, transit, activityTypes); - } - if (adapter != null) { - mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter); - } - } - - @Nullable - static Task findRootTaskFromContainer(WindowContainer wc) { - return wc.asTaskFragment() != null ? wc.asTaskFragment().getRootTask() - : wc.asActivityRecord().getRootTask(); - } - - @Nullable - static ActivityRecord getAppFromContainer(WindowContainer wc) { - return wc.asTaskFragment() != null ? wc.asTaskFragment().getTopNonFinishingActivity() - : wc.asActivityRecord(); - } - - /** - * @return The window token that determines the animation theme. - */ - @Nullable - private ActivityRecord findAnimLayoutParamsToken(@TransitionOldType int transit, - ArraySet<Integer> activityTypes, ArraySet<ActivityRecord> openingApps, - ArraySet<ActivityRecord> closingApps, ArraySet<WindowContainer> changingApps) { - ActivityRecord result; - - // Remote animations always win, but fullscreen tokens override non-fullscreen tokens. - result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps, - w -> w.getRemoteAnimationDefinition() != null - && w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes)); - if (result != null) { - return result; - } - result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps, - w -> w.fillsParent() && w.findMainWindow() != null); - if (result != null) { - return result; - } - return lookForHighestTokenWithFilter(closingApps, openingApps, changingApps, - w -> w.findMainWindow() != null); - } - - /** - * @return The set of {@link android.app.WindowConfiguration.ActivityType}s contained in the set - * of apps in {@code array1}, {@code array2}, and {@code array3}. - */ - private static ArraySet<Integer> collectActivityTypes(ArraySet<ActivityRecord> array1, - ArraySet<ActivityRecord> array2, ArraySet<WindowContainer> array3) { - final ArraySet<Integer> result = new ArraySet<>(); - for (int i = array1.size() - 1; i >= 0; i--) { - result.add(array1.valueAt(i).getActivityType()); - } - for (int i = array2.size() - 1; i >= 0; i--) { - result.add(array2.valueAt(i).getActivityType()); - } - for (int i = array3.size() - 1; i >= 0; i--) { - result.add(array3.valueAt(i).getActivityType()); - } - return result; - } - - private static ActivityRecord lookForHighestTokenWithFilter(ArraySet<ActivityRecord> array1, - ArraySet<ActivityRecord> array2, ArraySet<WindowContainer> array3, - Predicate<ActivityRecord> filter) { - final int array2base = array1.size(); - final int array3base = array2.size() + array2base; - final int count = array3base + array3.size(); - int bestPrefixOrderIndex = Integer.MIN_VALUE; - ActivityRecord bestToken = null; - for (int i = 0; i < count; i++) { - final WindowContainer wtoken = i < array2base - ? array1.valueAt(i) - : (i < array3base - ? array2.valueAt(i - array2base) - : array3.valueAt(i - array3base)); - final int prefixOrderIndex = wtoken.getPrefixOrderIndex(); - final ActivityRecord r = getAppFromContainer(wtoken); - if (r != null && filter.test(r) && prefixOrderIndex > bestPrefixOrderIndex) { - bestPrefixOrderIndex = prefixOrderIndex; - bestToken = r; - } - } - return bestToken; - } - - private boolean containsVoiceInteraction(ArraySet<ActivityRecord> apps) { - for (int i = apps.size() - 1; i >= 0; i--) { - if (apps.valueAt(i).mVoiceInteraction) { - return true; - } - } - return false; - } - - /** - * Apply animation to the set of window containers. - * - * @param wcs The list of {@link WindowContainer}s to which an app transition animation applies. - * @param apps The list of {@link ActivityRecord}s being transitioning. - * @param transit The current transition type. - * @param visible {@code true} if the apps becomes visible, {@code false} if the apps becomes - * invisible. - * @param animLp Layout parameters in which an app transition animation runs. - * @param voiceInteraction {@code true} if one of the apps in this transition belongs to a voice - * interaction session driving task. - */ - private void applyAnimations(ArraySet<WindowContainer> wcs, ArraySet<ActivityRecord> apps, - @TransitionOldType int transit, boolean visible, LayoutParams animLp, - boolean voiceInteraction) { - final int wcsCount = wcs.size(); - for (int i = 0; i < wcsCount; i++) { - final WindowContainer wc = wcs.valueAt(i); - // If app transition animation target is promoted to higher level, SurfaceAnimator - // triggers WC#onAnimationFinished only on the promoted target. So we need to take care - // of triggering AR#onAnimationFinished on each ActivityRecord which is a part of the - // app transition. - final ArrayList<ActivityRecord> transitioningDescendants = new ArrayList<>(); - for (int j = 0; j < apps.size(); ++j) { - final ActivityRecord app = apps.valueAt(j); - if (app.isDescendantOf(wc)) { - transitioningDescendants.add(app); - } - } - wc.applyAnimation(animLp, transit, visible, voiceInteraction, transitioningDescendants); - } - } - - /** - * Returns {@code true} if a given {@link WindowContainer} is an embedded Task in - * {@link TaskView}. - * - * Note that this is a short term workaround to support Android Auto until it migrate to - * ShellTransition. This should only be used by {@link #getAnimationTargets}. - * - * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled. - */ - static boolean isTaskViewTask(WindowContainer wc) { - // Use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and - // it is not guaranteed to work this logic in the future version. - boolean isTaskViewTask = wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer; - if (isTaskViewTask) { - return true; - } - - WindowContainer parent = wc.getParent(); - boolean isParentATaskViewTask = parent != null - && parent instanceof Task - && ((Task) parent).mRemoveWithTaskOrganizer; - return isParentATaskViewTask; - } - - /** - * Find WindowContainers to be animated from a set of opening and closing apps. We will promote - * animation targets to higher level in the window hierarchy if possible. - * - * @param visible {@code true} to get animation targets for opening apps, {@code false} to get - * animation targets for closing apps. - * @return {@link WindowContainer}s to be animated. - */ - @VisibleForTesting - static ArraySet<WindowContainer> getAnimationTargets( - ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps, - boolean visible) { - - // The candidates of animation targets, which might be able to promote to higher level. - final ArrayDeque<WindowContainer> candidates = new ArrayDeque<>(); - final ArraySet<ActivityRecord> apps = visible ? openingApps : closingApps; - for (int i = 0; i < apps.size(); ++i) { - final ActivityRecord app = apps.valueAt(i); - if (app.shouldApplyAnimation(visible)) { - candidates.add(app); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "Changing app %s visible=%b performLayout=%b", - app, app.isVisible(), false); - } - } - - final ArraySet<ActivityRecord> otherApps = visible ? closingApps : openingApps; - // Ancestors of closing apps while finding animation targets for opening apps, or ancestors - // of opening apps while finding animation targets for closing apps. - final ArraySet<WindowContainer> otherAncestors = new ArraySet<>(); - for (int i = 0; i < otherApps.size(); ++i) { - for (WindowContainer wc = otherApps.valueAt(i); wc != null; wc = wc.getParent()) { - otherAncestors.add(wc); - } - } - - // The final animation targets which cannot promote to higher level anymore. - final ArraySet<WindowContainer> targets = new ArraySet<>(); - final ArrayList<WindowContainer> siblings = new ArrayList<>(); - while (!candidates.isEmpty()) { - final WindowContainer current = candidates.removeFirst(); - final WindowContainer parent = current.getParent(); - siblings.clear(); - siblings.add(current); - boolean canPromote = true; - - if (isTaskViewTask(current)) { - // Don't animate an embedded Task in app transition. This is a short term workaround - // to prevent conflict of surface hierarchy changes between legacy app transition - // and TaskView (b/205189147). - // TODO(b/213312721): Remove this once ShellTransition is enabled. - continue; - } else if (parent == null || !parent.canCreateRemoteAnimationTarget() - // We cannot promote the animation on Task's parent when the task is in - // clearing task in case the animating get stuck when performing the opening - // task that behind it. - || (current.asTask() != null && current.asTask().mInRemoveTask) - // We cannot promote the animation to changing window. This may happen when an - // activity is open in a TaskFragment that is resizing, while the existing - // activity in the TaskFragment is reparented to another TaskFragment. - || parent.isChangingAppTransition()) { - canPromote = false; - } else { - // In case a descendant of the parent belongs to the other group, we cannot promote - // the animation target from "current" to the parent. - // - // Example: Imagine we're checking if we can animate a Task instead of a set of - // ActivityRecords. In case an activity starts a new activity within a same Task, - // an ActivityRecord of an existing activity belongs to the opening apps, at the - // same time, the other ActivityRecord of a new activity belongs to the closing - // apps. In this case, we cannot promote the animation target to Task level, but - // need to animate each individual activity. - // - // [Task] +- [ActivityRecord1] (in opening apps) - // +- [ActivityRecord2] (in closing apps) - if (otherAncestors.contains(parent)) { - canPromote = false; - } - - // If the current window container is a task with adjacent task set, the both - // adjacent tasks will be opened or closed together. To get their opening or - // closing animation target independently, skip promoting their animation targets. - if (current.asTask() != null && current.asTask().hasAdjacentTask()) { - canPromote = false; - } - - // Find all siblings of the current WindowContainer in "candidates", move them into - // a separate list "siblings", and checks if an animation target can be promoted - // to its parent. - // - // We can promote an animation target to its parent if and only if all visible - // siblings will be animating. - // - // Example: Imagine that a Task contains two visible activity record, but only one - // of them is included in the opening apps and the other belongs to neither opening - // or closing apps. This happens when an activity launches another translucent - // activity in the same Task. In this case, we cannot animate Task, but have to - // animate each activity, otherwise an activity behind the translucent activity also - // animates. - // - // [Task] +- [ActivityRecord1] (visible, in opening apps) - // +- [ActivityRecord2] (visible, not in opening apps) - for (int j = 0; j < parent.getChildCount(); ++j) { - final WindowContainer sibling = parent.getChildAt(j); - if (candidates.remove(sibling)) { - if (!isTaskViewTask(sibling)) { - // Don't animate an embedded Task in app transition. This is a short - // term workaround to prevent conflict of surface hierarchy changes - // between legacy app transition and TaskView (b/205189147). - // TODO(b/213312721): Remove this once ShellTransition is enabled. - siblings.add(sibling); - } - } else if (sibling != current && sibling.isVisible()) { - canPromote = false; - } - } - } - - if (canPromote) { - candidates.add(parent); - } else { - targets.addAll(siblings); - } - } - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, "getAnimationTarget in=%s, out=%s", - apps, targets); - return targets; - } - - /** - * Apply an app transition animation based on a set of {@link ActivityRecord} - * - * @param openingApps The list of opening apps to which an app transition animation applies. - * @param closingApps The list of closing apps to which an app transition animation applies. - * @param transit The current transition type. - * @param animLp Layout parameters in which an app transition animation runs. - * @param voiceInteraction {@code true} if one of the apps in this transition belongs to a voice - * interaction session driving task. - */ - private void applyAnimations(ArraySet<ActivityRecord> openingApps, - ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit, - LayoutParams animLp, boolean voiceInteraction) { - if (transit == WindowManager.TRANSIT_OLD_UNSET - || (openingApps.isEmpty() && closingApps.isEmpty())) { - return; - } - - if (AppTransition.isActivityTransitOld(transit)) { - final ArrayList<Pair<ActivityRecord, Rect>> closingLetterboxes = new ArrayList(); - for (int i = 0; i < closingApps.size(); ++i) { - ActivityRecord closingApp = closingApps.valueAt(i); - if (closingApp.areBoundsLetterboxed()) { - final Rect insets = closingApp.getLetterboxInsets(); - closingLetterboxes.add(new Pair(closingApp, insets)); - } - } - - for (int i = 0; i < openingApps.size(); ++i) { - ActivityRecord openingApp = openingApps.valueAt(i); - if (openingApp.areBoundsLetterboxed()) { - final Rect openingInsets = openingApp.getLetterboxInsets(); - for (Pair<ActivityRecord, Rect> closingLetterbox : closingLetterboxes) { - final Rect closingInsets = closingLetterbox.second; - if (openingInsets.equals(closingInsets)) { - ActivityRecord closingApp = closingLetterbox.first; - openingApp.setNeedsLetterboxedAnimation(true); - closingApp.setNeedsLetterboxedAnimation(true); - } - } - } - } - } - - final ArraySet<WindowContainer> openingWcs = getAnimationTargets( - openingApps, closingApps, true /* visible */); - final ArraySet<WindowContainer> closingWcs = getAnimationTargets( - openingApps, closingApps, false /* visible */); - applyAnimations(openingWcs, openingApps, transit, true /* visible */, animLp, - voiceInteraction); - applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp, - voiceInteraction); - - for (int i = 0; i < openingApps.size(); ++i) { - openingApps.valueAtUnchecked(i).mOverrideTaskTransition = false; - } - for (int i = 0; i < closingApps.size(); ++i) { - closingApps.valueAtUnchecked(i).mOverrideTaskTransition = false; - } - - final AccessibilityController accessibilityController = - mDisplayContent.mWmService.mAccessibilityController; - if (accessibilityController.hasCallbacks()) { - accessibilityController.onAppWindowTransition(mDisplayContent.getDisplayId(), transit); - } - } - - private void handleOpeningApps() { - final ArraySet<ActivityRecord> openingApps = mDisplayContent.mOpeningApps; - final int appsCount = openingApps.size(); - - for (int i = 0; i < appsCount; i++) { - final ActivityRecord app = openingApps.valueAt(i); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now opening app %s", app); - - app.commitVisibility(true /* visible */, false /* performLayout */); - - // In case a trampoline activity is used, it can happen that a new ActivityRecord is - // added and a new app transition starts before the previous app transition animation - // ends. So we cannot simply use app.isAnimating(PARENTS) to determine if the app must - // to be added to the list of tokens to be notified of app transition complete. - final WindowContainer wc = app.getAnimatingContainer(PARENTS, - ANIMATION_TYPE_APP_TRANSITION); - if (wc == null || !wc.getAnimationSources().contains(app)) { - // This token isn't going to be animating. Add it to the list of tokens to - // be notified of app transition complete since the notification will not be - // sent be the app window animator. - mDisplayContent.mNoAnimationNotifyOnTransitionFinished.add(app.token); - } - app.updateReportedVisibilityLocked(); - app.showAllWindowsLocked(); - - if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailUp()) { - app.attachThumbnailAnimation(); - } else if (mDisplayContent.mAppTransition.isNextAppTransitionOpenCrossProfileApps()) { - app.attachCrossProfileAppsThumbnailAnimation(); - } - } - } - - private void handleClosingApps() { - final ArraySet<ActivityRecord> closingApps = mDisplayContent.mClosingApps; - final int appsCount = closingApps.size(); - - for (int i = 0; i < appsCount; i++) { - final ActivityRecord app = closingApps.valueAt(i); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now closing app %s", app); - - app.commitVisibility(false /* visible */, false /* performLayout */); - app.updateReportedVisibilityLocked(); - // Force the allDrawn flag, because we want to start - // this guy's animations regardless of whether it's - // gotten drawn. - app.allDrawn = true; - // Ensure that apps that are mid-starting are also scheduled to have their - // starting windows removed after the animation is complete - if (app.mStartingWindow != null && !app.mStartingWindow.mAnimatingExit) { - app.removeStartingWindow(); - } - - if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailDown()) { - app.attachThumbnailAnimation(); - } - } - } - - private void handleClosingChangingContainers() { - final ArrayMap<WindowContainer, Rect> containers = - mDisplayContent.mClosingChangingContainers; - while (!containers.isEmpty()) { - final WindowContainer container = containers.keyAt(0); - containers.remove(container); - - // For closing changing windows that are part of the transition, they should have been - // removed from mClosingChangingContainers in WindowContainer#getAnimationAdapter() - // If the closing changing TaskFragment is not part of the transition, update its - // surface after removing it from mClosingChangingContainers. - final TaskFragment taskFragment = container.asTaskFragment(); - if (taskFragment != null) { - taskFragment.updateOrganizedTaskFragmentSurface(); - } - } - } - - private void handleChangingApps(@TransitionOldType int transit) { - final ArraySet<WindowContainer> apps = mDisplayContent.mChangingContainers; - final int appsCount = apps.size(); - for (int i = 0; i < appsCount; i++) { - WindowContainer wc = apps.valueAt(i); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now changing app %s", wc); - wc.applyAnimation(null, transit, true, false, null /* sources */); - } - } - - private boolean transitionGoodToGo(ArraySet<? extends WindowContainer> apps, - ArrayMap<WindowContainer, Integer> outReasons) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "Checking %d opening apps (timeout=%b)...", apps.size(), - mDisplayContent.mAppTransition.isTimeout()); - if (mDisplayContent.mAppTransition.isTimeout()) { - return true; - } - - for (int i = 0; i < apps.size(); i++) { - WindowContainer wc = apps.valueAt(i); - final ActivityRecord activity = getAppFromContainer(wc); - if (activity == null) { - continue; - } - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "Check opening app=%s: allDrawn=%b startingDisplayed=%b " - + "startingMoved=%b isRelaunching()=%b startingWindow=%s", - activity, activity.allDrawn, activity.isStartingWindowDisplayed(), - activity.startingMoved, activity.isRelaunching(), - activity.mStartingWindow); - final boolean allDrawn = activity.allDrawn && !activity.isRelaunching(); - if (!allDrawn && !activity.isStartingWindowDisplayed() && !activity.startingMoved) { - return false; - } - if (allDrawn) { - outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN); - } else { - outReasons.put(activity, - activity.mStartingData instanceof SplashScreenStartingData - ? APP_TRANSITION_SPLASH_SCREEN - : APP_TRANSITION_SNAPSHOT); - } - } - - // We also need to wait for the specs to be fetched, if needed. - if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true"); - return false; - } - - if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s", - mDisplayContent.mUnknownAppVisibilityController.getDebugMessage()); - return false; - } - - // If the wallpaper is visible, we need to check it's ready too. - return !mWallpaperControllerLocked.isWallpaperVisible() - || mWallpaperControllerLocked.wallpaperTransitionReady(); - } - - private boolean transitionGoodToGoForTaskFragments() { - if (mDisplayContent.mAppTransition.isTimeout()) { - return true; - } - - // Check all Tasks in this transition. This is needed because new TaskFragment created for - // launching activity may not be in the tracking lists, but we still want to wait for the - // activity launch to start the transition. - final ArraySet<Task> rootTasks = new ArraySet<>(); - for (int i = mDisplayContent.mOpeningApps.size() - 1; i >= 0; i--) { - rootTasks.add(mDisplayContent.mOpeningApps.valueAt(i).getRootTask()); - } - for (int i = mDisplayContent.mClosingApps.size() - 1; i >= 0; i--) { - rootTasks.add(mDisplayContent.mClosingApps.valueAt(i).getRootTask()); - } - for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) { - rootTasks.add( - findRootTaskFromContainer(mDisplayContent.mChangingContainers.valueAt(i))); - } - - // Organized TaskFragment can be empty for two situations: - // 1. New created and is waiting for Activity launch. In this case, we want to wait for - // the Activity launch to trigger the transition. - // 2. Last Activity is just removed. In this case, we want to wait for organizer to - // remove the TaskFragment because it may also want to change other TaskFragments in - // the same transition. - for (int i = rootTasks.size() - 1; i >= 0; i--) { - final Task rootTask = rootTasks.valueAt(i); - if (rootTask == null) { - // It is possible that one activity may have been removed from the hierarchy. No - // need to check for this case. - continue; - } - final boolean notReady = rootTask.forAllLeafTaskFragments(taskFragment -> { - if (!taskFragment.isReadyToTransit()) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Organized TaskFragment is not ready= %s", - taskFragment); - return true; - } - return false; - }); - if (notReady) { - return false; - } - } - return true; - } - - /** - * Identifies whether the current transition occurs within a single task or not. This is used - * to determine whether animations should be clipped to the task bounds instead of root task - * bounds. - */ - @VisibleForTesting - boolean isTransitWithinTask(@TransitionOldType int transit, Task task) { - if (task == null - || !mDisplayContent.mChangingContainers.isEmpty()) { - // if there is no task, then we can't constrain to the task. - // if anything is changing, it can animate outside its task. - return false; - } - if (!(transit == TRANSIT_OLD_ACTIVITY_OPEN - || transit == TRANSIT_OLD_ACTIVITY_CLOSE - || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH)) { - // only activity-level transitions will be within-task. - return false; - } - // check that all components are in the task. - for (ActivityRecord activity : mDisplayContent.mOpeningApps) { - Task activityTask = activity.getTask(); - if (activityTask != task) { - return false; - } - } - for (ActivityRecord activity : mDisplayContent.mClosingApps) { - if (activity.getTask() != task) { - return false; - } - } - return true; - } - - private static boolean canBeWallpaperTarget(ArraySet<ActivityRecord> apps) { - for (int i = apps.size() - 1; i >= 0; i--) { - if (apps.valueAt(i).windowsCanBeWallpaperTarget()) { - return true; - } - } - return false; - } - - /** - * Finds the top app in a list of apps, using its {@link ActivityRecord#getPrefixOrderIndex} to - * compare z-order. - * - * @param apps The list of apps to search. - * @param ignoreInvisible If set to true, ignores apps that are not - * {@link ActivityRecord#isVisible}. - * @return The top {@link ActivityRecord}. - */ - private static ActivityRecord getTopApp(ArraySet<? extends WindowContainer> apps, - boolean ignoreInvisible) { - int topPrefixOrderIndex = Integer.MIN_VALUE; - ActivityRecord topApp = null; - for (int i = apps.size() - 1; i >= 0; i--) { - final ActivityRecord app = getAppFromContainer(apps.valueAt(i)); - if (app == null || ignoreInvisible && !app.isVisible()) { - continue; - } - final int prefixOrderIndex = app.getPrefixOrderIndex(); - if (prefixOrderIndex > topPrefixOrderIndex) { - topPrefixOrderIndex = prefixOrderIndex; - topApp = app; - } - } - return topApp; - } -} diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 79bed3d8453d..e76a83453a9d 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -388,8 +388,7 @@ class BackNavigationController { removedWindowContainer); mBackAnimationInProgress = builder != null; if (mBackAnimationInProgress) { - if (removedWindowContainer.mTransitionController.inTransition() - || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) { + if (removedWindowContainer.mTransitionController.inTransition()) { ProtoLog.w(WM_DEBUG_BACK_PREVIEW, "Pending back animation due to another animation is running"); mPendingAnimationBuilder = builder; @@ -817,6 +816,8 @@ class BackNavigationController { if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) && ar.mTransitionController.isCollecting(ar)) { final TransitionController controller = ar.mTransitionController; + final Transition transition = controller.getCollectingTransition(); + final int switchType = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType; boolean collectTask = false; ActivityRecord changedActivity = null; for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { @@ -829,8 +830,16 @@ class BackNavigationController { changedActivity = next; } } - if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType - == AnimationHandler.TASK_SWITCH) { + if (Flags.unifyBackNavigationTransition()) { + for (int i = mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mOpenAnimAdaptor.mAdaptors[i].mTarget, + false /* isTop */); + } + collectAnimatableTarget(transition, switchType, + mAnimationHandler.mCloseAdaptor.mTarget, true /* isTop */); + } + if (collectTask && switchType == AnimationHandler.TASK_SWITCH) { final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); if (topTask != null) { WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); @@ -848,6 +857,18 @@ class BackNavigationController { } } + private static void collectAnimatableTarget(Transition transition, int switchType, + WindowContainer animatingTarget, boolean isTop) { + if ((switchType == AnimationHandler.ACTIVITY_SWITCH + && (animatingTarget.asActivityRecord() != null + || animatingTarget.asTaskFragment() != null)) + || (switchType == AnimationHandler.TASK_SWITCH + && animatingTarget.asTask() != null)) { + transition.collect(animatingTarget); + transition.setBackGestureAnimation(animatingTarget, isTop); + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -992,8 +1013,8 @@ class BackNavigationController { return; } - if (mWindowManagerService.mRoot.mTransitionController.isCollecting()) { - Slog.v(TAG, "Skip predictive back transition, another transition is collecting"); + if (mWindowManagerService.mRoot.mTransitionController.inTransition()) { + Slog.v(TAG, "Skip predictive back transition, another transition is playing"); cancelPendingAnimation(); return; } @@ -1098,7 +1119,7 @@ class BackNavigationController { } final Transition prepareTransition = builder.prepareTransitionIfNeeded( - openingActivities); + openingActivities, close, open); final SurfaceControl.Transaction st = openingActivities[0].getSyncTransaction(); final SurfaceControl.Transaction ct = prepareTransition != null ? st : close.getPendingTransaction(); @@ -1790,7 +1811,8 @@ class BackNavigationController { return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget); } - private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) { + private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities, + WindowContainer promoteToClose, WindowContainer[] promoteToOpen) { if (Flags.unifyBackNavigationTransition()) { if (mCloseTarget.asWindowState() != null) { return null; @@ -1806,11 +1828,11 @@ class BackNavigationController { final TransitionController tc = visibleOpenActivities[0].mTransitionController; final Transition prepareOpen = tc.createTransition( TRANSIT_PREPARE_BACK_NAVIGATION); - tc.collect(mCloseTarget); - prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */); - for (int i = mOpenTargets.length - 1; i >= 0; --i) { - tc.collect(mOpenTargets[i]); - prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */); + tc.collect(promoteToClose); + prepareOpen.setBackGestureAnimation(promoteToClose, true /* isTop */); + for (int i = promoteToOpen.length - 1; i >= 0; --i) { + tc.collect(promoteToOpen[i]); + prepareOpen.setBackGestureAnimation(promoteToOpen[i], false /* isTop */); } if (!makeVisibles.isEmpty()) { setLaunchBehind(visibleOpenActivities); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 6fa92176959e..682f3d8cf1e5 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -226,7 +226,6 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.MagnificationSpec; import android.view.PrivacyIndicatorBounds; -import android.view.RemoteAnimationDefinition; import android.view.RoundedCorners; import android.view.Surface; import android.view.Surface.Rotation; @@ -367,8 +366,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private int mMaxUiWidth = 0; final AppTransition mAppTransition; - final AppTransitionController mAppTransitionController; - boolean mSkipAppTransitionAnimation = false; final ArraySet<ActivityRecord> mOpeningApps = new ArraySet<>(); final ArraySet<ActivityRecord> mClosingApps = new ArraySet<>(); @@ -547,9 +544,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // TODO(multi-display): remove some of the usages. boolean isDefaultDisplay; - /** Indicates whether any presentation is shown on this display. */ - boolean mIsPresenting; - /** Save allocating when calculating rects */ private final Rect mTmpRect = new Rect(); private final Region mTmpRegion = new Region(); @@ -1164,7 +1158,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mAppTransition = new AppTransition(mWmService.mContext, mWmService, this); mAppTransition.registerListenerLocked(mWmService.mActivityManagerAppTransitionNotifier); mAppTransition.registerListenerLocked(mFixedRotationTransitionListener); - mAppTransitionController = new AppTransitionController(mWmService, this); mTransitionController.registerLegacyListener(mFixedRotationTransitionListener); mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this); mRemoteDisplayChangeController = new RemoteDisplayChangeController(this); @@ -1556,10 +1549,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return mInputMethodSurfaceParentWindow; } - void registerRemoteAnimations(RemoteAnimationDefinition definition) { - mAppTransitionController.registerRemoteAnimations(definition); - } - void reconfigureDisplayLocked() { if (!isReady()) { return; @@ -5607,20 +5596,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** - * Transfer app transition from other display to this display. - * - * @param from Display from where the app transition is transferred. - * - * TODO(new-app-transition): Remove this once the shell handles app transition. - */ - void transferAppTransitionFrom(DisplayContent from) { - final boolean prepared = mAppTransition.transferFrom(from.mAppTransition); - if (prepared && okToAnimate()) { - mSkipAppTransitionAnimation = false; - } - } - - /** * @deprecated new transition should use {@link #requestTransitionAndLegacyPrepare(int, int)} */ @Deprecated @@ -5634,10 +5609,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Deprecated void prepareAppTransition(@WindowManager.TransitionType int transit, @WindowManager.TransitionFlags int flags) { - final boolean prepared = mAppTransition.prepareAppTransition(transit, flags); - if (prepared && okToAnimate() && transit != TRANSIT_NONE) { - mSkipAppTransitionAnimation = false; - } + mAppTransition.prepareAppTransition(transit, flags); } /** @@ -7112,14 +7084,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void updateRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) { - int newRequestedVisibleTypes = + @InsetsType int updateRequestedVisibleTypes( + @InsetsType int visibleTypes, @InsetsType int mask) { + final int newRequestedVisibleTypes = (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask); if (mRequestedVisibleTypes != newRequestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ newRequestedVisibleTypes; mRequestedVisibleTypes = newRequestedVisibleTypes; + return changedTypes; } + return 0; } } diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 907d0dc2e183..7b6fc9e5694d 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -34,6 +34,7 @@ import android.util.proto.ProtoOutputStream; import android.view.InputApplicationHandle; import android.view.InputChannel; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.window.InputTransferToken; import com.android.internal.protolog.ProtoLog; @@ -260,7 +261,7 @@ class EmbeddedWindowController { // The EmbeddedWindow can only request the IME. All other insets types are requested by // the host window. - private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0; + private @InsetsType int mRequestedVisibleTypes = 0; /** Whether the gesture is transferred to embedded window. */ boolean mGestureToEmbedded = false; @@ -354,24 +355,28 @@ class EmbeddedWindowController { } @Override - public boolean isRequestedVisible(@WindowInsets.Type.InsetsType int types) { + public boolean isRequestedVisible(@InsetsType int types) { return (mRequestedVisibleTypes & types) != 0; } @Override - public @WindowInsets.Type.InsetsType int getRequestedVisibleTypes() { + public @InsetsType int getRequestedVisibleTypes() { return mRequestedVisibleTypes; } /** * Only the IME can be requested from the EmbeddedWindow. - * @param requestedVisibleTypes other types than {@link WindowInsets.Type.IME} are + * @param requestedVisibleTypes other types than {@link WindowInsets.Type#ime()} are * not sent to system server via WindowlessWindowManager. + * @return an integer as the changed requested visible insets types. */ - void setRequestedVisibleTypes(@WindowInsets.Type.InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @Override diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index ef1f58eafacc..b4d55a160631 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -437,9 +437,9 @@ class InsetsPolicy { return originalState; } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { - mStateController.onRequestedVisibleTypesChanged(caller, statsToken); + mStateController.onRequestedVisibleTypesChanged(caller, changedTypes, statsToken); checkAbortTransient(caller); updateBarControlTarget(mFocusedWin); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 9202cf2d5792..164abab992d8 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -219,14 +219,20 @@ class InsetsStateController { } } - void onRequestedVisibleTypesChanged(InsetsTarget caller, + void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes, @Nullable ImeTracker.Token statsToken) { boolean changed = false; for (int i = mProviders.size() - 1; i >= 0; i--) { final InsetsSourceProvider provider = mProviders.valueAt(i); - final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime(); - changed |= provider.updateClientVisibility(caller, - isImeProvider ? statsToken : null); + final @InsetsType int type = provider.getSource().getType(); + if ((type & changedTypes) != 0) { + final boolean isImeProvider = type == WindowInsets.Type.ime(); + changed |= provider.updateClientVisibility( + caller, isImeProvider ? statsToken : null) + // Fake control target cannot change the client visibility, but it should + // change the insets with its newly requested visibility. + || (caller == provider.getFakeControlTarget()); + } } if (changed) { notifyInsetsChanged(); @@ -435,7 +441,8 @@ class InsetsStateController { for (int i = newControlTargets.size() - 1; i >= 0; i--) { // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here - onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), null /* statsToken */); + onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), + WindowInsets.Type.all(), null /* statsToken */); } newControlTargets.clear(); if (!android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java new file mode 100644 index 000000000000..69463433827f --- /dev/null +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; + +import android.annotation.NonNull; +import android.util.IntArray; + +import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.WmProtoLogGroups; + +/** + * Manages presentation windows. + */ +class PresentationController { + + // TODO(b/395475549): Add support for display add/remove, and activity move across displays. + private final IntArray mPresentingDisplayIds = new IntArray(); + + PresentationController() {} + + private boolean isPresenting(int displayId) { + return mPresentingDisplayIds.contains(displayId); + } + + boolean shouldOccludeActivities(int displayId) { + // All activities on the presenting display must be hidden so that malicious apps can't do + // tap jacking (b/391466268). + // For now, this should only be applied to external displays because presentations can only + // be shown on them. + // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that + // the presentation won't stop its controlling activity. + return enablePresentationForConnectedDisplays() && isPresenting(displayId); + } + + void onPresentationAdded(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (isPresenting(displayId)) { + return; + } + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s", + win.getDisplayId(), win); + mPresentingDisplayIds.add(win.getDisplayId()); + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true); + } + + void onPresentationRemoved(@NonNull WindowState win) { + final int displayId = win.getDisplayId(); + if (!isPresenting(displayId)) { + return; + } + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, + "Presentation removed from display %d: %s", win.getDisplayId(), win); + // TODO(b/393945496): Make sure that there's one presentation at most per display. + final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId); + if (displayIdIndex != -1) { + mPresentingDisplayIds.remove(displayIdIndex); + } + if (enablePresentationForConnectedDisplays()) { + // A presentation hides all activities behind on the same display. + win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, + /*notifyClients=*/ true); + } + win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); + } +} diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 95d9b3e612ac..c93efd327096 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -35,7 +35,6 @@ import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_OCCLUDING; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; -import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_WAKE; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_FOCUS_LIGHT; @@ -68,7 +67,6 @@ import static com.android.server.wm.ActivityTaskSupervisor.DEFER_RESUME; import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP; import static com.android.server.wm.ActivityTaskSupervisor.dumpHistoryList; import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity; -import static com.android.server.wm.KeyguardController.KEYGUARD_SLEEP_TOKEN_TAG; import static com.android.server.wm.RootWindowContainerProto.IS_HOME_RECENTS_COMPONENT; import static com.android.server.wm.RootWindowContainerProto.KEYGUARD_CONTROLLER; import static com.android.server.wm.RootWindowContainerProto.WINDOW_CONTAINER; @@ -803,8 +801,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> mWmService.mAtmService.mTaskFragmentOrganizerController.dispatchPendingEvents(); mWmService.mSyncEngine.onSurfacePlacement(); - checkAppTransitionReady(surfacePlacer); - mWmService.mAtmService.mBackNavigationController .checkAnimationReady(defaultDisplay.mWallpaperController); @@ -898,38 +894,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> if (DEBUG_WINDOW_TRACE) Slog.e(TAG, "performSurfacePlacementInner exit"); } - private void checkAppTransitionReady(WindowSurfacePlacer surfacePlacer) { - // Trace all displays app transition by Z-order for pending layout change. - for (int i = mChildren.size() - 1; i >= 0; --i) { - final DisplayContent curDisplay = mChildren.get(i); - - // If we are ready to perform an app transition, check through all of the app tokens - // to be shown and see if they are ready to go. - if (curDisplay.mAppTransition.isReady()) { - // handleAppTransitionReady may modify curDisplay.pendingLayoutChanges. - curDisplay.mAppTransitionController.handleAppTransitionReady(); - if (DEBUG_LAYOUT_REPEATS) { - surfacePlacer.debugLayoutRepeats("after handleAppTransitionReady", - curDisplay.pendingLayoutChanges); - } - } - - if (curDisplay.mAppTransition.isRunning() && !curDisplay.isAppTransitioning()) { - // We have finished the animation of an app transition. To do this, we have - // delayed a lot of operations like showing and hiding apps, moving apps in - // Z-order, etc. - // The app token list reflects the correct Z-order, but the window list may now - // be out of sync with it. So here we will just rebuild the entire app window - // list. Fun! - curDisplay.handleAnimatingStoppedAndTransition(); - if (DEBUG_LAYOUT_REPEATS) { - surfacePlacer.debugLayoutRepeats("after handleAnimStopAndXitionLock", - curDisplay.pendingLayoutChanges); - } - } - } - } - private void applySurfaceChangesTransaction() { // TODO(multi-display): Support these features on secondary screens. final DisplayContent defaultDc = mDefaultDisplay; @@ -2266,20 +2230,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // Ensure the leash of new task is in sync with its current bounds after reparent. rootTask.maybeApplyLastRecentsAnimationTransaction(); - - // In the case of this activity entering PIP due to it being moved to the back, - // the old activity would have a TRANSIT_TASK_TO_BACK transition that needs to be - // ran. But, since its visibility did not change (note how it was STOPPED/not - // visible, and with it now at the back stack, it remains not visible), the logic to - // add the transition is automatically skipped. We then add this activity manually - // to the list of apps being closed, and request its transition to be ran. - final ActivityRecord oldTopActivity = task.getTopMostActivity(); - if (oldTopActivity != null && oldTopActivity.isState(STOPPED) - && task.getDisplayContent().mAppTransition.containsTransitRequest( - TRANSIT_TO_BACK)) { - task.getDisplayContent().mClosingApps.add(oldTopActivity); - oldTopActivity.mRequestForceTransition = true; - } } // TODO(remove-legacy-transit): Move this to the `singleActivity` case when removing @@ -2958,20 +2908,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> display.mAllSleepTokens.remove(token); if (display.mAllSleepTokens.isEmpty()) { mService.updateSleepIfNeededLocked(); - // Assuming no lock screen is set and a user launches an activity, turns off the screen - // and turn on the screen again, then the launched activity should be displayed on the - // screen without app transition animation. When the screen turns on, both keyguard - // sleep token and display off sleep token are removed, but the order is - // non-deterministic. - // Note: Display#mSkipAppTransitionAnimation will be ignored when keyguard related - // transition exists, so this affects only when no lock screen is set. Otherwise - // keyguard going away animation will be played. - // See also AppTransitionController#getTransitCompatType for more details. - if ((!mTaskSupervisor.getKeyguardController().isKeyguardOccluded(display.mDisplayId) - && token.mTag.equals(KEYGUARD_SLEEP_TOKEN_TAG)) - || token.mTag.equals(DISPLAY_OFF_SLEEP_TOKEN_TAG)) { - display.mSkipAppTransitionAnimation = true; - } } } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 1ad5988e3c2e..8d198b26f396 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -704,9 +704,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); } - win.setRequestedVisibleTypes(requestedVisibleTypes); + final @InsetsType int changedTypes = + win.setRequestedVisibleTypes(requestedVisibleTypes); win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win, - imeStatsToken); + changedTypes, imeStatsToken); final Task task = win.getTask(); if (task != null) { task.dispatchTaskInfoChangedIfNeeded(/* forced= */ true); @@ -723,10 +724,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { // TODO(b/353463205) Use different phase here ImeTracker.forLogging().onProgress(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); - embeddedWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = embeddedWindow.setRequestedVisibleTypes( requestedVisibleTypes & WindowInsets.Type.ime()); embeddedWindow.getDisplayContent().getInsetsPolicy() - .onRequestedVisibleTypesChanged(embeddedWindow, imeStatsToken); + .onRequestedVisibleTypesChanged( + embeddedWindow, changedTypes, imeStatsToken); } else { ImeTracker.forLogging().onFailed(imeStatsToken, ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES); diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java index dcdffa416e33..2664dcd3ae3f 100644 --- a/services/core/java/com/android/server/wm/SnapshotController.java +++ b/services/core/java/com/android/server/wm/SnapshotController.java @@ -72,11 +72,6 @@ class SnapshotController { mActivitySnapshotController.notifyAppVisibilityChanged(appWindowToken, visible); } - // For legacy transition, which won't support activity snapshot - void onTransitionStarting(DisplayContent displayContent) { - mTaskSnapshotController.handleClosingApps(displayContent.mClosingApps); - } - // For shell transition, record snapshots before transaction start. void onTransactionReady(@WindowManager.TransitionType int type, ArrayList<Transition.ChangeInfo> changeInfos) { diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index 3dfff39e9b68..c5425fedf2ac 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -132,10 +132,7 @@ public class SurfaceAnimator { animationFinishCallback.onAnimationFinished(type, anim); } }; - // If both the Animatable and AnimationAdapter requests to be deferred, only the - // first one will be called. - if (!(mAnimatable.shouldDeferAnimationFinish(resetAndInvokeFinish) - || anim.shouldDeferAnimationFinish(resetAndInvokeFinish))) { + if (!anim.shouldDeferAnimationFinish(resetAndInvokeFinish)) { resetAndInvokeFinish.run(); } mAnimationFinished = true; @@ -639,23 +636,5 @@ public class SurfaceAnimator { * @return The height of the surface to be animated. */ int getSurfaceHeight(); - - /** - * Gets called when the animation is about to finish and gives the client the opportunity to - * defer finishing the animation, i.e. it keeps the leash around until the client calls - * {@link #cancelAnimation}. - * <p> - * {@link AnimationAdapter} has a similar method which is called only if this method returns - * false. This mean that if both this {@link Animatable} and the {@link AnimationAdapter} - * request to be deferred, this method is the sole responsible to call - * endDeferFinishCallback. On the other hand, the animation finish might still be deferred - * if this method return false and the one from the {@link AnimationAdapter} returns true. - * - * @param endDeferFinishCallback The callback to call when defer finishing should be ended. - * @return Whether the client would like to defer the animation finish. - */ - default boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { - return false; - } } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index f75e7175b4d2..3abab8bf62c2 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -508,9 +508,6 @@ class Task extends TaskFragment { */ boolean mAllowForceResizeOverride = true; - private final AnimatingActivityRegistry mAnimatingActivityRegistry = - new AnimatingActivityRegistry(); - private static final int TRANSLUCENT_TIMEOUT_MSG = FIRST_ACTIVITY_TASK_MSG + 1; private final Handler mHandler; @@ -1122,17 +1119,6 @@ class Task extends TaskFragment { // already ran fully within super.onParentChanged updateTaskOrganizerState(); - // TODO(b/168037178): The check for null display content and setting it to null doesn't - // really make sense here... - - // TODO(b/168037178): This is mostly taking care of the case where the stask is removing - // from the display, so we should probably consolidate it there instead. - - if (getParent() == null && mDisplayContent != null) { - mDisplayContent = null; - mWmService.mWindowPlacerLocked.requestTraversal(); - } - if (oldParent != null) { final Task oldParentTask = oldParent.asTask(); if (oldParentTask != null) { @@ -1185,9 +1171,6 @@ class Task extends TaskFragment { } mRootWindowContainer.updateUIDsPresentOnDisplay(); - - // Ensure all animations are finished at same time in split-screen mode. - forAllActivities(ActivityRecord::updateAnimatingActivityRegistry); } @Override @@ -2770,6 +2753,7 @@ class Task extends TaskFragment { } super.removeImmediately(); + mDisplayContent = null; mRemoving = false; } @@ -3345,13 +3329,6 @@ class Task extends TaskFragment { mLastSurfaceShowing = show; } - @Override - void dump(PrintWriter pw, String prefix, boolean dumpAll) { - super.dump(pw, prefix, dumpAll); - mAnimatingActivityRegistry.dump(pw, "AnimatingApps:", prefix); - } - - /** * Fills in a {@link TaskInfo} with information from this task. Note that the base intent in the * task info will not include any extras or clip data. @@ -6313,10 +6290,6 @@ class Task extends TaskFragment { return mDisplayContent.getDisplayInfo(); } - AnimatingActivityRegistry getAnimatingActivityRegistry() { - return mAnimatingActivityRegistry; - } - private Rect getRawBounds() { return super.getBounds(); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 324852d1a410..97a1a34336e9 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -394,6 +394,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ private boolean mAllowTransitionWhenEmpty; + /** + * Specifies which configuration changes should trigger TaskFragment info changed callbacks. + * Only system TaskFragment organizers are allowed to set this value. + */ + private @ActivityInfo.Config int mConfigurationChangeMaskForOrganizer; + /** When set, will force the task to report as invisible. */ static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1; static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1; @@ -656,6 +662,17 @@ class TaskFragment extends WindowContainer<WindowContainer> { mAllowTransitionWhenEmpty = allowTransitionWhenEmpty; } + void setConfigurationChangeMaskForOrganizer(@ActivityInfo.Config int mask) { + // Only system organizers are allowed to set configuration change mask. + if (mTaskFragmentOrganizerController.isSystemOrganizer(mTaskFragmentOrganizer.asBinder())) { + mConfigurationChangeMaskForOrganizer = mask; + } + } + + @ActivityInfo.Config int getConfigurationChangeMaskForOrganizer() { + return mConfigurationChangeMaskForOrganizer; + } + /** @see #mIsolatedNav */ boolean isIsolatedNav() { return isEmbedded() && mIsolatedNav; diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index e63107cdc720..ae329d787156 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -349,8 +349,10 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr // Check if the info is different from the last reported info. final TaskFragmentInfo info = tf.getTaskFragmentInfo(); final TaskFragmentInfo lastInfo = mLastSentTaskFragmentInfos.get(tf); - if (info.equalsForTaskFragmentOrganizer(lastInfo) && configurationsAreEqualForOrganizer( - info.getConfiguration(), lastInfo.getConfiguration())) { + final int configurationChangeMask = tf.getConfigurationChangeMaskForOrganizer(); + if (info.equalsForTaskFragmentOrganizer(lastInfo) + && configurationsAreEqualForOrganizer(info.getConfiguration(), + lastInfo.getConfiguration(), configurationChangeMask)) { return null; } diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index 432ed1d0b61d..8a937721b347 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -113,27 +113,6 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot enableLowResSnapshots, lowResScaleFactor, use16BitFormat); } - // Still needed for legacy transition.(AppTransitionControllerTest) - void handleClosingApps(ArraySet<ActivityRecord> closingApps) { - if (shouldDisableSnapshots()) { - return; - } - // We need to take a snapshot of the task if and only if all activities of the task are - // either closing or hidden. - mTmpTasks.clear(); - for (int i = closingApps.size() - 1; i >= 0; i--) { - final ActivityRecord activity = closingApps.valueAt(i); - if (activity.isActivityTypeHome()) continue; - final Task task = activity.getTask(); - if (task == null) continue; - - getClosingTasksInner(task, mTmpTasks); - } - snapshotTasks(mTmpTasks); - mTmpTasks.clear(); - mSkipClosingAppSnapshotTasks.clear(); - } - /** * Adds the given {@param tasks} to the list of tasks which should not have their snapshots * taken upon the next processing of the set of closing apps. The caller is responsible for diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 55c2668f62d0..7af542f10127 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3359,7 +3359,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, boolean isVoiceInteraction) { - if (AppTransitionController.isTaskViewTask(this) || (isOrganized() + if ((isOrganized() // TODO(b/161711458): Clean-up when moved to shell. && getWindowingMode() != WINDOWING_MODE_FULLSCREEN && getWindowingMode() != WINDOWING_MODE_FREEFORM diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d5626661725e..d699a689459e 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -157,7 +157,6 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.setScPropertiesInClient; @@ -348,7 +347,6 @@ import com.android.server.AnimationThread; import com.android.server.DisplayThread; import com.android.server.FgThread; import com.android.server.LocalServices; -import com.android.server.SystemConfig; import com.android.server.UiThread; import com.android.server.Watchdog; import com.android.server.input.InputManagerService; @@ -450,11 +448,6 @@ public class WindowManagerService extends IWindowManager.Stub /** * Use WMShell for app transition. */ - private static final String ENABLE_SHELL_TRANSITIONS = "persist.wm.debug.shell_transit"; - - /** - * @see #ENABLE_SHELL_TRANSITIONS - */ public static final boolean sEnableShellTransitions = getShellTransitEnabled(); /** @@ -503,6 +496,8 @@ public class WindowManagerService extends IWindowManager.Stub final StartingSurfaceController mStartingSurfaceController; + final PresentationController mPresentationController; + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { @@ -1433,6 +1428,7 @@ public class WindowManagerService extends IWindowManager.Stub setGlobalShadowSettings(); mAnrController = new AnrController(this); mStartingSurfaceController = new StartingSurfaceController(this); + mPresentationController = new PresentationController(); mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); @@ -1937,16 +1933,8 @@ public class WindowManagerService extends IWindowManager.Stub } outSizeCompatScale[0] = win.getCompatScaleForClient(); - if (res >= ADD_OKAY - && (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION)) { - displayContent.mIsPresenting = true; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - displayContent.ensureActivitiesVisible(/*starting=*/ null, - /*notifyClients=*/ true); - } - mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(), - /*isShown=*/ true); + if (res >= ADD_OKAY && win.isPresentation()) { + mPresentationController.onPresentationAdded(win); } } @@ -4732,11 +4720,13 @@ public class WindowManagerService extends IWindowManager.Stub } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES); - dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(visibleTypes, mask); + final @InsetsType int changedTypes = + dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes( + visibleTypes, mask); // TODO(b/353463205) the statsToken shouldn't be null as it is used later in the // IME provider. Check if we have to create a new request here, if null. dc.getInsetsStateController().onRequestedVisibleTypesChanged( - dc.mRemoteInsetsControlTarget, statsToken); + dc.mRemoteInsetsControlTarget, changedTypes, statsToken); } } finally { Binder.restoreCallingIdentity(origId); @@ -10315,11 +10305,6 @@ public class WindowManagerService extends IWindowManager.Stub } private static boolean getShellTransitEnabled() { - android.content.pm.FeatureInfo autoFeature = SystemConfig.getInstance() - .getAvailableFeatures().get(PackageManager.FEATURE_AUTOMOTIVE); - if (autoFeature != null && autoFeature.version >= 0) { - return SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, true); - } return true; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 924b9de5a562..3b6a4dc6e1b0 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -2455,10 +2455,28 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub /** Whether the configuration changes are important to report back to an organizer. */ static boolean configurationsAreEqualForOrganizer( Configuration newConfig, @Nullable Configuration oldConfig) { + return configurationsAreEqualForOrganizer(newConfig, oldConfig, 0 /* additionalMask */); + } + + /** + * Whether the configuration changes are important to report back to an organizer. + * + * @param newConfig the new configuration + * @param oldConfig the old configuration + * @param additionalMask specifies additional configuration changes that the organizer is + * interested in. If the configuration change matches any bit in the mask, + * {@code false} is returned. + */ + static boolean configurationsAreEqualForOrganizer( + Configuration newConfig, @Nullable Configuration oldConfig, + @ActivityInfo.Config int additionalMask) { if (oldConfig == null) { return false; } int cfgChanges = newConfig.diff(oldConfig); + if ((cfgChanges & additionalMask) != 0) { + return false; + } final int winCfgChanges = (cfgChanges & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0 ? (int) newConfig.windowConfiguration.diff(oldConfig.windowConfiguration, true /* compareUndefined */) : 0; @@ -2665,6 +2683,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub ownerActivity.getUid(), ownerActivity.info.processName); if (mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken.asBinder())) { taskFragment.setOverrideOrientation(creationParams.getOverrideOrientation()); + taskFragment.setConfigurationChangeMaskForOrganizer( + creationParams.getConfigurationChangeMask()); } final int position; if (creationParams.getPairedPrimaryFragmentToken() != null) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index f94bbb70befc..589724182980 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -182,7 +182,6 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; -import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -822,17 +821,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } /** + * @return an integer as the changed requested visible insets types. * @see #getRequestedVisibleTypes() */ - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { if (mRequestedVisibleTypes != requestedVisibleTypes) { + final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes; mRequestedVisibleTypes = requestedVisibleTypes; + return changedTypes; } + return 0; } @VisibleForTesting - void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) { - setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); + @InsetsType int setRequestedVisibleTypes( + @InsetsType int requestedVisibleTypes, @InsetsType int mask) { + return setRequestedVisibleTypes( + mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); } /** @@ -2294,15 +2299,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int type = mAttrs.type; - if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) { - // TODO(b/393945496): Make sure that there's one presentation at most per display. - dc.mIsPresenting = false; - if (enablePresentationForConnectedDisplays()) { - // A presentation hides all activities behind on the same display. - dc.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); - } - mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(), - /*isShown=*/ false); + if (isPresentation()) { + mWmService.mPresentationController.onPresentationRemoved(this); } // Check if window provides non decor insets before clearing its provided insets. final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets(); @@ -3331,6 +3329,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } } + boolean isPresentation() { + return mAttrs.type == TYPE_PRESENTATION || mAttrs.type == TYPE_PRIVATE_PRESENTATION; + } + private boolean isOnVirtualDisplay() { return getDisplayContent().mDisplay.getType() == Display.TYPE_VIRTUAL; } diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 2aa0c6b6dd0b..440eae5f7dea 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -446,7 +446,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential @Override public void binderDied() { Slog.d(TAG, "Client binder died - clearing session"); - finishSession(isUiWaitingForData(), ApiStatus.CLIENT_CANCELED.getMetricCode()); + finishSession(isUiWaitingForData(), ApiStatus.BINDER_DIED.getMetricCode()); } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java index ece729fe5b32..c21e645d797f 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java @@ -16,6 +16,7 @@ package com.android.server.credentials.metrics; +import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_BINDER_DIED; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_CLIENT_CANCELED; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_FAILURE; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_SUCCESS; @@ -27,7 +28,9 @@ public enum ApiStatus { CLIENT_CANCELED( CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_CLIENT_CANCELED), USER_CANCELED( - CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_USER_CANCELED); + CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_USER_CANCELED), + BINDER_DIED( + CREDENTIAL_MANAGER_FINAL_PHASE_REPORTED__API_STATUS__API_STATUS_BINDER_DIED); private final int mInnerMetricCode; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java index 6e038f9b67a0..ba02122d1dc5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java @@ -54,17 +54,7 @@ class EnterpriseSpecificIdCalculator { TelephonyManager telephonyService = context.getSystemService(TelephonyManager.class); Preconditions.checkState(telephonyService != null, "Unable to access telephony service"); - String imei; - try { - imei = telephonyService.getImei(0); - } catch (UnsupportedOperationException doesNotSupportGms) { - // Instead of catching the exception, we could check for FEATURE_TELEPHONY_GSM. - // However that runs the risk of changing a device's existing ESID if on these devices - // telephonyService.getImei() actually returns non-null even when the device does not - // declare FEATURE_TELEPHONY_GSM. - imei = null; - } - mImei = imei; + mImei = telephonyService.getImei(0); String meid; try { meid = telephonyService.getMeid(0); diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp index dae481a3c215..36947a2a6d62 100644 --- a/services/incremental/IncrementalService.cpp +++ b/services/incremental/IncrementalService.cpp @@ -3198,8 +3198,10 @@ void IncrementalService::DataLoaderStub::onDump(int fd) { dprintf(fd, " }\n"); } -void IncrementalService::AppOpsListener::opChanged(int32_t, const String16&) { +binder::Status IncrementalService::AppOpsListener::opChanged(int32_t, int32_t, + const String16&, const String16&) { incrementalService.onAppOpChanged(packageName); + return binder::Status::ok(); } binder::Status IncrementalService::IncrementalServiceConnector::setStorageParams( diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h index b81e1b1b071c..4ee1a70dc34c 100644 --- a/services/incremental/IncrementalService.h +++ b/services/incremental/IncrementalService.h @@ -26,7 +26,7 @@ #include <android/os/incremental/BnStorageLoadingProgressListener.h> #include <android/os/incremental/PerUidReadTimeouts.h> #include <android/os/incremental/StorageHealthCheckParams.h> -#include <binder/IAppOpsCallback.h> +#include <binder/AppOpsManager.h> #include <binder/PersistableBundle.h> #include <utils/String16.h> #include <utils/StrongPointer.h> @@ -200,11 +200,12 @@ public: void getMetrics(int32_t storageId, android::os::PersistableBundle* _aidl_return); - class AppOpsListener : public android::BnAppOpsCallback { + class AppOpsListener : public com::android::internal::app::BnAppOpsCallback { public: AppOpsListener(IncrementalService& incrementalService, std::string packageName) : incrementalService(incrementalService), packageName(std::move(packageName)) {} - void opChanged(int32_t op, const String16& packageName) final; + binder::Status opChanged(int32_t op, int32_t uid, const String16& packageName, + const String16& persistentDeviceId) final; private: IncrementalService& incrementalService; diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h index 39e2ee324e0c..36a5b7f4a75d 100644 --- a/services/incremental/ServiceWrappers.h +++ b/services/incremental/ServiceWrappers.h @@ -23,7 +23,7 @@ #include <android/content/pm/IDataLoader.h> #include <android/content/pm/IDataLoaderStatusListener.h> #include <android/os/incremental/PerUidReadTimeouts.h> -#include <binder/IAppOpsCallback.h> +#include <binder/AppOpsManager.h> #include <binder/IServiceManager.h> #include <binder/Status.h> #include <incfs.h> @@ -133,6 +133,7 @@ public: class AppOpsManagerWrapper { public: + using IAppOpsCallback = ::com::android::internal::app::IAppOpsCallback; virtual ~AppOpsManagerWrapper() = default; virtual binder::Status checkPermission(const char* permission, const char* operation, const char* package) const = 0; diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp index d9d3d62e92e2..73849a3e0e00 100644 --- a/services/incremental/test/IncrementalServiceTest.cpp +++ b/services/incremental/test/IncrementalServiceTest.cpp @@ -1678,7 +1678,7 @@ TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsSuccessAndPermissionChang {}, {})); ASSERT_GE(mDataLoader->setStorageParams(true), 0); ASSERT_NE(nullptr, mAppOpsManager->mStoredCallback.get()); - mAppOpsManager->mStoredCallback->opChanged(0, {}); + mAppOpsManager->mStoredCallback->opChanged(0, 0, {}, {}); } TEST_F(IncrementalServiceTest, testSetIncFsMountOptionsCheckPermissionFails) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c974d9e1dc87..2bbd69c65eb8 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -894,6 +894,17 @@ public final class SystemServer implements Dumpable { SystemServiceRegistry.sEnableServiceNotFoundWtf = true; + // Prepare the thread pool for init tasks that can be parallelized + SystemServerInitThreadPool tp = SystemServerInitThreadPool.start(); + mDumper.addDumpable(tp); + + if (android.server.Flags.earlySystemConfigInit()) { + // SystemConfig init is expensive, so enqueue the work as early as possible to allow + // concurrent execution before it's needed (typically by ActivityManagerService). + // As native library loading is also expensive, this is a good place to start. + startSystemConfigInit(t); + } + // Initialize native services. System.loadLibrary("android_servers"); @@ -926,9 +937,6 @@ public final class SystemServer implements Dumpable { mDumper.addDumpable(mSystemServiceManager); LocalServices.addService(SystemServiceManager.class, mSystemServiceManager); - // Prepare the thread pool for init tasks that can be parallelized - SystemServerInitThreadPool tp = SystemServerInitThreadPool.start(); - mDumper.addDumpable(tp); // Lazily load the pre-installed system font map in SystemServer only if we're not doing // the optimized font loading in the FontManagerService. @@ -1093,6 +1101,14 @@ public final class SystemServer implements Dumpable { } } + private void startSystemConfigInit(TimingsTraceAndSlog t) { + Slog.i(TAG, "Reading configuration..."); + final String tagSystemConfig = "ReadingSystemConfig"; + t.traceBegin(tagSystemConfig); + SystemServerInitThreadPool.submit(SystemConfig::getInstance, tagSystemConfig); + t.traceEnd(); + } + private void createSystemContext() { ActivityThread activityThread = ActivityThread.systemMain(); mSystemContext = activityThread.getSystemContext(); @@ -1131,11 +1147,11 @@ public final class SystemServer implements Dumpable { mDumper.addDumpable(watchdog); t.traceEnd(); - Slog.i(TAG, "Reading configuration..."); - final String TAG_SYSTEM_CONFIG = "ReadingSystemConfig"; - t.traceBegin(TAG_SYSTEM_CONFIG); - SystemServerInitThreadPool.submit(SystemConfig::getInstance, TAG_SYSTEM_CONFIG); - t.traceEnd(); + // Legacy entry point for starting SystemConfig init, only needed if the early init flag is + // disabled and we haven't already triggered init before bootstrap services. + if (!android.server.Flags.earlySystemConfigInit()) { + startSystemConfigInit(t); + } // Orchestrates some ProtoLogging functionality. if (android.tracing.Flags.clientSideProtoLogging()) { diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 4d021ec2c0d3..86ccd878de7c 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -10,6 +10,13 @@ flag { } flag { + namespace: "system_performance" + name: "early_system_config_init" + description: "Perform earlier initialization of SystemConfig in system server startup." + bug: "383869534" +} + +flag { name: "remove_text_service" namespace: "wear_frameworks" description: "Remove TextServiceManagerService on Wear" diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index a103b0583eac..a7280c2167ea 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -31,7 +31,6 @@ import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import android.app.Instrumentation; -import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; import android.os.RemoteException; @@ -42,7 +41,6 @@ import android.provider.Settings; import android.server.wm.WindowManagerStateHelper; import android.util.Log; import android.view.WindowManagerGlobal; -import android.view.WindowManagerPolicyConstants; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; @@ -59,6 +57,7 @@ import androidx.test.uiautomator.Until; import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper; import com.android.apps.inputmethod.simpleime.testing.TestActivity; +import com.android.compatibility.common.util.GestureNavSwitchHelper; import com.android.compatibility.common.util.SystemUtil; import org.junit.After; @@ -90,6 +89,8 @@ public class InputMethodServiceTest { private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); + private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); + private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider(); @Rule @@ -100,7 +101,6 @@ public class InputMethodServiceTest { private Instrumentation mInstrumentation; private UiDevice mUiDevice; - private Context mContext; private InputMethodManager mImm; private String mTargetPackageName; private String mInputMethodId; @@ -112,8 +112,7 @@ public class InputMethodServiceTest { public void setUp() throws Exception { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mUiDevice = UiDevice.getInstance(mInstrumentation); - mContext = mInstrumentation.getContext(); - mImm = mContext.getSystemService(InputMethodManager.class); + mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class); mTargetPackageName = mInstrumentation.getTargetContext().getPackageName(); mInputMethodId = getInputMethodId(); prepareIme(); @@ -872,35 +871,47 @@ public class InputMethodServiceTest { * Verifies that clicking on the IME navigation bar back button hides the IME. */ @Test - public void testBackButtonClick() { + public void testBackButtonClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var backButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButtonUiObject.click(); - mInstrumentation.waitForIdleSync(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithWindowInsetsController(), + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); + backButton.click(); + mInstrumentation.waitForIdleSync(); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have + // to wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse(); + } + } finally { + restoreNav[0].close(); } } @@ -908,35 +919,47 @@ public class InputMethodServiceTest { * Verifies that long clicking on the IME navigation bar back button hides the IME. */ @Test - public void testBackButtonLongClick() { + public void testBackButtonLongClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var backButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButtonUiObject.longClick(); - mInstrumentation.waitForIdleSync(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithWindowInsetsController(), + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); + backButton.longClick(); + mInstrumentation.waitForIdleSync(); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have + // to wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertWithMessage("IME is not shown") + .that(mInputMethodService.isInputViewShown()).isFalse(); + } + } finally { + restoreNav[0].close(); } } @@ -945,74 +968,104 @@ public class InputMethodServiceTest { * or switches the input method. */ @Test - public void testImeSwitchButtonClick() { + public void testImeSwitchButtonClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var initialInfo = mImm.getCurrentInputMethodInfo(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - final var imeSwitchButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); - imeSwitchButtonUiObject.click(); - mInstrumentation.waitForIdleSync(); + verifyInputViewStatusOnMainSync( + () -> { + setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + mActivity.showImeWithWindowInsetsController(); + }, + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); - final var newInfo = mImm.getCurrentInputMethodInfo(); + final var initialInfo = mImm.getCurrentInputMethodInfo(); - assertWithMessage("Input Method Switcher Menu is shown or input method was switched") - .that(isInputMethodPickerShown(mImm) || !Objects.equals(initialInfo, newInfo)) - .isTrue(); + final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); + imeSwitcherButton.click(); + mInstrumentation.waitForIdleSync(); - assertWithMessage("IME is still shown after IME Switcher button was clicked") - .that(mInputMethodService.isInputViewShown()).isTrue(); + final var newInfo = mImm.getCurrentInputMethodInfo(); + + assertWithMessage("Input Method Switcher Menu is shown or input method was switched") + .that(isInputMethodPickerShown(mImm) || !Objects.equals(initialInfo, newInfo)) + .isTrue(); + + assertWithMessage("IME is still shown after IME Switcher button was clicked") + .that(mInputMethodService.isInputViewShown()).isTrue(); - // Hide the IME Switcher Menu before finishing. - mUiDevice.pressBack(); + // Hide the IME Switcher Menu before finishing. + mUiDevice.pressBack(); + } finally { + restoreNav[0].close(); + } } /** * Verifies that long clicking on the IME switch button shows the Input Method Switcher Menu. */ @Test - public void testImeSwitchButtonLongClick() { + public void testImeSwitchButtonLongClick() throws Exception { assumeTrue("Must have a navigation bar", hasNavigationBar()); - assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled()); waitUntilActivityReadyForInputInjection(mActivity); setShowImeWithHardKeyboard(true /* enabled */); - verifyInputViewStatusOnMainSync( - () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); - mActivity.showImeWithWindowInsetsController(); - }, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + final boolean isGestureMode = mGestureNavSwitchHelper.isGestureMode(); - final var imeSwitchButtonUiObject = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); - imeSwitchButtonUiObject.longClick(); - mInstrumentation.waitForIdleSync(); + final var restoreNav = new AutoCloseable[]{() -> {}}; + try { + if (!isGestureMode) { + // Wait for onConfigurationChanged when changing navigation modes. + verifyInputViewStatus( + () -> restoreNav[0] = mGestureNavSwitchHelper.withGestureNavigationMode(), + true, /* expected */ + false /* inputViewStarted */ + ); + } - assertWithMessage("Input Method Switcher Menu is shown") - .that(isInputMethodPickerShown(mImm)).isTrue(); - assertWithMessage("IME is still shown after IME Switcher button was long clicked") - .that(mInputMethodService.isInputViewShown()).isTrue(); + verifyInputViewStatusOnMainSync( + () -> { + setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + mActivity.showImeWithWindowInsetsController(); + }, + true /* expected */, + true /* inputViewStarted */); + assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + + final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); + imeSwitcherButton.longClick(); + mInstrumentation.waitForIdleSync(); - // Hide the IME Switcher Menu before finishing. - mUiDevice.pressBack(); + assertWithMessage("Input Method Switcher Menu is shown") + .that(isInputMethodPickerShown(mImm)).isTrue(); + assertWithMessage("IME is still shown after IME Switcher button was long clicked") + .that(mInputMethodService.isInputViewShown()).isTrue(); + + // Hide the IME Switcher Menu before finishing. + mUiDevice.pressBack(); + } finally { + restoreNav[0].close(); + } } private void verifyInputViewStatus(@NonNull Runnable runnable, boolean expected, @@ -1105,6 +1158,9 @@ public class InputMethodServiceTest { // Get the new TestActivity. mActivity = TestActivity.getLastCreatedInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); + // Wait for the new EditText to be served by InputMethodManager. + eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") + .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); } verifyInputViewStatusOnMainSync( @@ -1214,18 +1270,12 @@ public class InputMethodServiceTest { return uiObject; } - /** Checks whether gesture navigation move is enabled. */ - private boolean isGestureNavEnabled() { - return mContext.getResources().getInteger( - com.android.internal.R.integer.config_navBarInteractionMode) - == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; - } - /** Checks whether the device has a navigation bar on the IME's display. */ private boolean hasNavigationBar() { try { return WindowManagerGlobal.getWindowManagerService() - .hasNavigationBar(mInputMethodService.getDisplayId()); + .hasNavigationBar(mInputMethodService.getDisplayId()) + && mGestureNavSwitchHelper.hasNavigationBar(); } catch (RemoteException e) { fail("Failed to check whether the device has a navigation bar: " + e.getMessage()); return false; diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml index cf7d660a68ef..00873de4aaed 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml @@ -42,6 +42,7 @@ <activity android:name="com.android.apps.inputmethod.simpleime.testing.TestActivity" android:exported="false" android:label="TestActivity" + android:configChanges="assetsPaths" android:launchMode="singleInstance" android:excludeFromRecents="true" android:noHistory="true" diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index f154dbcee21a..09ce263e9b2f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -3962,7 +3962,7 @@ public class DisplayModeDirectorTest { } @Override - public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) { + public VotesStatsReporter getVotesStatsReporter() { return null; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java index 1e665c2c5c50..409706b14c56 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java @@ -1550,6 +1550,118 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2)); } + @SuppressWarnings("GuardedBy") + @Test + public void testDeliveryGroupPolicy_sameAction_multiplePolicies() { + // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_GREEN + // package. + final Intent greenPackageChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN)); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "com.example.green/10002", only the most recent one + // gets delivered and the rest get discarded. + final BroadcastOptions optionsMostRecentPolicyForPackageGreen = + BroadcastOptions.makeBasic(); + optionsMostRecentPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN)); + optionsMostRecentPolicyForPackageGreen.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_RED + // package. + final Intent redPackageChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_RED), List.of(PACKAGE_RED)); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "com.example.red/10001", only the most recent one + // gets delivered and the rest get discarded. + final BroadcastOptions optionsMostRecentPolicyForPackageRed = + BroadcastOptions.makeBasic(); + optionsMostRecentPolicyForPackageRed.setDeliveryGroupMatchingKey("package_changed", + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED)); + optionsMostRecentPolicyForPackageRed.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of + // PACKAGE_GREEN package. + final Intent greenPackageComponentsChangedIntent1 = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), + List.of(PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2")); + final Intent greenPackageComponentsChangedIntent2 = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), + List.of(PACKAGE_GREEN + ".comp3")); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "components-com.example.green/10002", merge the extras + // within these broadcasts such that only one broadcast is sent and the rest are + // discarded. Couple of things to note here: + // 1. We are intentionally using a different policy group + // "components-com.example.green/10002" (as opposed to "com.example.green/10002" used + // earlier), because this is corresponding to a change in some particular components, + // rather than a change to the whole package and we want to keep these two types of + // broadcasts independent. + // 2. We are using 'extrasMerger' to indicate how we want the extras to be merged. This + // assumes that broadcasts belonging to the group 'components-com.example.green/10002' + // will have the same values for all the extras, except for the one extra + // 'EXTRA_CHANGED_COMPONENT_NAME_LIST'. So, we explicitly specify how to merge this + // extra by using 'STRATEGY_ARRAY_APPEND' strategy, which basically indicates that + // the extra values which are arrays should be concatenated. + final BundleMerger extrasMerger = new BundleMerger(); + extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + BundleMerger.STRATEGY_ARRAY_APPEND); + final BroadcastOptions optionsMergedPolicyForPackageGreen = BroadcastOptions.makeBasic(); + optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + "components-" + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN)); + optionsMergedPolicyForPackageGreen.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsMergedPolicyForPackageGreen.setDeliveryGroupExtrasMerger(extrasMerger); + + // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of + // PACKAGE_RED package. + final Intent redPackageComponentsChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_RED), + List.of(PACKAGE_RED + ".comp1", PACKAGE_RED + ".comp2")); + // Create delivery group policy such that when there are multiple broadcasts within the + // delivery group identified by "components-com.example.red/10001", merge the extras + // within these broadcasts such that only one broadcast is sent and the rest are + // discarded. + final BroadcastOptions optionsMergedPolicyForPackageRed = BroadcastOptions.makeBasic(); + optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed", + "components-" + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED)); + optionsMergedPolicyForPackageRed.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsMergedPolicyForPackageRed.setDeliveryGroupExtrasMerger(extrasMerger); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent, + optionsMostRecentPolicyForPackageGreen)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageChangedIntent, + optionsMostRecentPolicyForPackageRed)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent1, + optionsMergedPolicyForPackageGreen)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageComponentsChangedIntent, + optionsMergedPolicyForPackageRed)); + // Since this broadcast has DELIVERY_GROUP_MOST_RECENT policy set, the earlier + // greenPackageChangedIntent broadcast with the same policy will be discarded. + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent, + optionsMostRecentPolicyForPackageGreen)); + // Since this broadcast has DELIVERY_GROUP_MERGED policy set, the earlier + // greenPackageComponentsChangedIntent1 broadcast with the same policy will be merged + // with this one and then will be discarded. + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent2, + optionsMergedPolicyForPackageGreen)); + + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + // The extra EXTRA_CHANGED_COMPONENT_NAME_LIST values from + // greenPackageComponentsChangedIntent1 and + // greenPackageComponentsChangedIntent2 broadcasts would be merged, since + // STRATEGY_ARRAY_APPEND was used for this extra. + final Intent expectedGreenPackageComponentsChangedIntent = createPackageChangedIntent( + getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN + ".comp3", + PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2")); + verifyPendingRecords(queue, List.of(redPackageChangedIntent, + redPackageComponentsChangedIntent, greenPackageChangedIntent, + expectedGreenPackageComponentsChangedIntent)); + } + private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs, int droppedCount) { final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index f79cb1105611..360d6ebfe1bd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -19,6 +19,11 @@ import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE; import static android.content.pm.PackageManager.FEATURE_EMBEDDED; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static android.multiuser.Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION; +import static android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES; +import static android.multiuser.Flags.FLAG_LOGOUT_USER_API; +import static android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE; +import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE; import static android.os.UserManager.DISALLOW_OUTGOING_CALLS; import static android.os.UserManager.DISALLOW_SMS; import static android.os.UserManager.DISALLOW_USER_SWITCH; @@ -54,7 +59,6 @@ import android.content.Context; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; import android.content.res.Resources; -import android.multiuser.Flags; import android.os.PowerManager; import android.os.ServiceSpecificException; import android.os.SystemProperties; @@ -401,15 +405,27 @@ public final class UserManagerServiceTest { } @Test - public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception { + public void testGetBootUser_CannotSwitchToHeadlessSystemUser_ThrowsIfOnlySystemUserExists() + throws Exception { setSystemUserHeadless(true); removeNonSystemUsers(); + mockCanSwitchToHeadlessSystemUser(false); assertThrows(UserManager.CheckedUserOperationException.class, () -> mUmi.getBootUser(/* waitUntilSet= */ false)); } @Test + public void testGetBootUser_CanSwitchToHeadlessSystemUser_NoThrowIfOnlySystemUserExists() + throws Exception { + setSystemUserHeadless(true); + removeNonSystemUsers(); + mockCanSwitchToHeadlessSystemUser(true); + + assertThat(mUmi.getBootUser(/* waitUntilSet= */ false)).isEqualTo(UserHandle.USER_SYSTEM); + } + + @Test public void testGetPreviousFullUserToEnterForeground() throws Exception { addUser(USER_ID); setLastForegroundTime(USER_ID, 1_000_000L); @@ -601,9 +617,8 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) public void testAutoLockPrivateProfile() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); int mainUser = mUms.getMainUserId(); assumeTrue(mUms.canAddPrivateProfile(mainUser)); UserManagerService mSpiedUms = spy(mUms); @@ -622,10 +637,12 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES, + FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE + }) public void testAutoLockOnDeviceLockForPrivateProfile() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); int mainUser = mUms.getMainUserId(); assumeTrue(mUms.canAddPrivateProfile(mainUser)); UserManagerService mSpiedUms = spy(mUms); @@ -645,10 +662,12 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES, + FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE + }) public void testAutoLockOnDeviceLockForPrivateProfile_keyguardUnlocked() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); assumeTrue(mUms.canAddPrivateProfile(0)); UserManagerService mSpiedUms = spy(mUms); UserInfo privateProfileUser = @@ -665,10 +684,9 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({FLAG_ALLOW_PRIVATE_PROFILE, FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @DisableFlags(FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE) public void testAutoLockOnDeviceLockForPrivateProfile_flagDisabled() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.disableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); int mainUser = mUms.getMainUserId(); assumeTrue(mUms.canAddPrivateProfile(mainUser)); UserManagerService mSpiedUms = spy(mUms); @@ -687,10 +705,12 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES, + FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE + }) public void testAutoLockAfterInactityForPrivateProfile() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); int mainUser = mUms.getMainUserId(); assumeTrue(mUms.canAddPrivateProfile(mainUser)); UserManagerService mSpiedUms = spy(mUms); @@ -711,11 +731,12 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES, + FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE + }) public void testSetOrUpdateAutoLockPreference_noPrivateProfile() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); - mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); @@ -726,10 +747,12 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES, + FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE + }) public void testSetOrUpdateAutoLockPreference() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); int mainUser = mUms.getMainUserId(); assumeTrue(mUms.canAddPrivateProfile(mainUser)); mUms.createProfileForUserEvenWhenDisallowedWithThrow(PRIVATE_PROFILE_NAME, @@ -780,10 +803,12 @@ public final class UserManagerServiceTest { } @Test + @EnableFlags({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES, + android.multiuser.Flags.FLAG_ENABLE_HIDING_PROFILES + }) public void testGetProfileIdsExcludingHidden() { - mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_HIDING_PROFILES); assumeTrue(mUms.canAddPrivateProfile(0)); UserInfo privateProfileUser = mUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", @@ -794,8 +819,11 @@ public final class UserManagerServiceTest { } @Test - @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @RequiresFlagsEnabled({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_BLOCK_PRIVATE_SPACE_CREATION, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES + }) public void testCreatePrivateProfileOnHeadlessSystemUser_shouldAllowCreation() { UserManagerService mSpiedUms = spy(mUms); assumeTrue(mUms.isHeadlessSystemUserMode()); @@ -807,8 +835,11 @@ public final class UserManagerServiceTest { } @Test - @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @RequiresFlagsEnabled({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_BLOCK_PRIVATE_SPACE_CREATION, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES + }) public void testCreatePrivateProfileOnSecondaryUser_shouldNotAllowCreation() { assumeTrue(mUms.canAddMoreUsersOfType(USER_TYPE_FULL_SECONDARY)); UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0); @@ -819,8 +850,11 @@ public final class UserManagerServiceTest { } @Test - @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @RequiresFlagsEnabled({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_BLOCK_PRIVATE_SPACE_CREATION, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES + }) public void testCreatePrivateProfileOnAutoDevices_shouldNotAllowCreation() { doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_AUTOMOTIVE), anyInt()); int mainUser = mUms.getMainUserId(); @@ -831,8 +865,11 @@ public final class UserManagerServiceTest { } @Test - @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @RequiresFlagsEnabled({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_BLOCK_PRIVATE_SPACE_CREATION, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES + }) public void testCreatePrivateProfileOnTV_shouldNotAllowCreation() { doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_LEANBACK), anyInt()); int mainUser = mUms.getMainUserId(); @@ -843,8 +880,11 @@ public final class UserManagerServiceTest { } @Test - @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @RequiresFlagsEnabled({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_BLOCK_PRIVATE_SPACE_CREATION, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES + }) public void testCreatePrivateProfileOnEmbedded_shouldNotAllowCreation() { doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_EMBEDDED), anyInt()); int mainUser = mUms.getMainUserId(); @@ -855,8 +895,11 @@ public final class UserManagerServiceTest { } @Test - @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) + @RequiresFlagsEnabled({ + FLAG_ALLOW_PRIVATE_PROFILE, + FLAG_BLOCK_PRIVATE_SPACE_CREATION, + FLAG_ENABLE_PRIVATE_SPACE_FEATURES + }) public void testCreatePrivateProfileOnWatch_shouldNotAllowCreation() { doReturn(true).when(mMockPms).hasSystemFeature(eq(FEATURE_WATCH), anyInt()); int mainUser = mUms.getMainUserId(); @@ -910,7 +953,7 @@ public final class UserManagerServiceTest { } @Test - @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + @EnableFlags(FLAG_LOGOUT_USER_API) public void testGetUserLogoutability_HsumAndInteractiveHeadlessSystem_UserCanLogout() throws Exception { setSystemUserHeadless(true); @@ -926,7 +969,7 @@ public final class UserManagerServiceTest { } @Test - @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + @EnableFlags(FLAG_LOGOUT_USER_API) public void testGetUserLogoutability_HsumAndNonInteractiveHeadlessSystem_UserCannotLogout() throws Exception { setSystemUserHeadless(true); @@ -941,7 +984,7 @@ public final class UserManagerServiceTest { } @Test - @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + @EnableFlags(FLAG_LOGOUT_USER_API) public void testGetUserLogoutability_Hsum_SystemUserCannotLogout() throws Exception { setSystemUserHeadless(true); mockCurrentUser(UserHandle.USER_SYSTEM); @@ -950,7 +993,7 @@ public final class UserManagerServiceTest { } @Test - @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + @EnableFlags(FLAG_LOGOUT_USER_API) public void testGetUserLogoutability_NonHsum_SystemUserCannotLogout() throws Exception { setSystemUserHeadless(false); mockCurrentUser(UserHandle.USER_SYSTEM); @@ -960,7 +1003,7 @@ public final class UserManagerServiceTest { } @Test - @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + @EnableFlags(FLAG_LOGOUT_USER_API) public void testGetUserLogoutability_CannotSwitch_CannotLogout() throws Exception { setSystemUserHeadless(true); addUser(USER_ID); @@ -973,7 +1016,7 @@ public final class UserManagerServiceTest { } @Test - @DisableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + @DisableFlags(FLAG_LOGOUT_USER_API) public void testGetUserLogoutability_LogoutDisabled() throws Exception { assertThrows(UnsupportedOperationException.class, () -> mUms.getUserLogoutability(USER_ID)); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index 7b8824cb0e3d..00cc7264c1d0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -18,6 +18,11 @@ package com.android.server.accessibility.autoclick; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface; + import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -59,11 +64,25 @@ public class AutoclickTypePanelTest { private LinearLayout mDragButton; private LinearLayout mScrollButton; + private @AutoclickType int mActiveClickType = AUTOCLICK_TYPE_LEFT_CLICK; + + private final ClickPanelControllerInterface clickPanelController = + new ClickPanelControllerInterface() { + @Override + public void handleAutoclickTypeChange(@AutoclickType int clickType) { + mActiveClickType = clickType; + } + + @Override + public void toggleAutoclickPause() {} + }; + @Before public void setUp() { mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); - mAutoclickTypePanel = new AutoclickTypePanel(mTestableContext, mMockWindowManager); + mAutoclickTypePanel = + new AutoclickTypePanel(mTestableContext, mMockWindowManager, clickPanelController); View contentView = mAutoclickTypePanel.getContentViewForTesting(); mLeftClickButton = contentView.findViewById(R.id.accessibility_autoclick_left_click_layout); mRightClickButton = @@ -136,6 +155,17 @@ public class AutoclickTypePanelTest { verifyButtonHasSelectedStyle(mScrollButton); } + @Test + public void togglePanelExpansion_selectButton_correctActiveClickType() { + // By first click, the panel is expanded. + mLeftClickButton.callOnClick(); + + // Clicks any button in the expanded state to select a type button. + mScrollButton.callOnClick(); + + assertThat(mActiveClickType).isEqualTo(AUTOCLICK_TYPE_SCROLL); + } + private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) { GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground(); assertThat(gradientDrawable.getColor().getDefaultColor()) diff --git a/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java b/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java index 17f5ebb3b02a..7349c5f463a6 100644 --- a/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java +++ b/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java @@ -116,6 +116,10 @@ public class GameManagerServiceSettingsTests { deleteFolder(InstrumentationRegistry.getTargetContext().getFilesDir()); } + static { + System.loadLibrary("servicestestjni"); + } + @Test public void testReadGameServiceSettings() { writeOldFiles(); diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index 04d075211297..a4e77c00d647 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -20,10 +20,12 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.hardware.contexthub.EndpointInfo; import android.hardware.contexthub.ErrorCode; import android.hardware.contexthub.HubEndpointInfo; import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier; @@ -32,6 +34,7 @@ import android.hardware.contexthub.IContextHubEndpoint; import android.hardware.contexthub.IContextHubEndpointCallback; import android.hardware.contexthub.IEndpointCommunication; import android.hardware.contexthub.MessageDeliveryStatus; +import android.hardware.contexthub.Reason; import android.os.Binder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -61,6 +64,9 @@ public class ContextHubEndpointTest { private static final int ENDPOINT_ID = 1; private static final String ENDPOINT_PACKAGE_NAME = "com.android.server.location.contexthub"; + private static final String TARGET_ENDPOINT_NAME = "Example target endpoint"; + private static final int TARGET_ENDPOINT_ID = 1; + private ContextHubClientManager mClientManager; private ContextHubEndpointManager mEndpointManager; private HubInfoRegistry mHubInfoRegistry; @@ -95,23 +101,8 @@ public class ContextHubEndpointTest { @Test public void testRegisterEndpoint() throws RemoteException { - // Register an endpoint and confirm we can get a valid IContextHubEndoint reference - HubEndpointInfo info = - new HubEndpointInfo( - ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList()); - IContextHubEndpoint endpoint = - mEndpointManager.registerEndpoint( - info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null); - assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1); - assertThat(endpoint).isNotNull(); - HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); - assertThat(assignedInfo).isNotNull(); - HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); - assertThat(assignedIdentifier).isNotNull(); - - // Unregister the endpoint and confirm proper clean-up - mEndpointManager.unregisterEndpoint(assignedIdentifier.getEndpoint()); - assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + unregisterExampleEndpoint(endpoint); } @Test @@ -146,4 +137,107 @@ public class ContextHubEndpointTest { assertThat(statusCaptor.getValue().messageSequenceNumber).isEqualTo(sequenceNumber); assertThat(statusCaptor.getValue().errorCode).isEqualTo(ErrorCode.DESTINATION_NOT_FOUND); } + + @Test + public void testHalRestart() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + // Verify that the endpoint is still registered after a HAL restart + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + mEndpointManager.onHalRestart(); + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications, times(2)).registerEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub()); + + unregisterExampleEndpoint(endpoint); + } + + @Test + public void testHalRestartOnOpenSession() throws RemoteException { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); + mEndpointManager.onEndpointSessionOpenComplete(sessionId); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1); + + mEndpointManager.onHalRestart(); + + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications, times(2)).registerEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub()); + + verify(mMockCallback) + .onSessionClosed( + sessionId, ContextHubServiceUtil.toAppHubEndpointReason(Reason.HUB_RESET)); + + unregisterExampleEndpoint(endpoint); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + } + + @Test + public void testOpenSessionOnUnregistration() throws RemoteException { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + IContextHubEndpoint endpoint = registerExampleEndpoint(); + + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); + mEndpointManager.onEndpointSessionOpenComplete(sessionId); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1); + + unregisterExampleEndpoint(endpoint); + verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.ENDPOINT_GONE); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + } + + private IContextHubEndpoint registerExampleEndpoint() throws RemoteException { + HubEndpointInfo info = + new HubEndpointInfo( + ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList()); + IContextHubEndpoint endpoint = + mEndpointManager.registerEndpoint( + info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null); + assertThat(endpoint).isNotNull(); + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + assertThat(assignedInfo).isNotNull(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + assertThat(assignedIdentifier).isNotNull(); + + // Confirm registerEndpoint was called with the right contents + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications).registerEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id).isEqualTo(assignedIdentifier.getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId).isEqualTo(assignedIdentifier.getHub()); + assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1); + + return endpoint; + } + + private void unregisterExampleEndpoint(IContextHubEndpoint endpoint) throws RemoteException { + HubEndpointInfo expectedInfo = endpoint.getAssignedHubEndpointInfo(); + endpoint.unregister(); + ArgumentCaptor<EndpointInfo> statusCaptor = ArgumentCaptor.forClass(EndpointInfo.class); + verify(mMockEndpointCommunications).unregisterEndpoint(statusCaptor.capture()); + assertThat(statusCaptor.getValue().id.id) + .isEqualTo(expectedInfo.getIdentifier().getEndpoint()); + assertThat(statusCaptor.getValue().id.hubId) + .isEqualTo(expectedInfo.getIdentifier().getHub()); + assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java index 770712a191fd..41011928f8b3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java @@ -204,7 +204,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase { .build()); final Notification n = new Notification.Builder(getContext()) .setContentTitle("foo") - .setCategory(CATEGORY_ALARM) + .setCategory(new String("alarm")) .setSmallIcon(android.R.drawable.sym_def_app_icon) .build(); NotificationRecord r = getRecord(channel, n); 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 440f43e9b926..bb296148dad1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2881,7 +2881,6 @@ public class ActivityRecordTests extends WindowTestsBase { activity2.addStartingWindow(mPackageName, android.R.style.Theme, activity1, true, true, false, true, false, false, false); waitUntilHandlersIdle(); - assertFalse(mDisplayContent.mSkipAppTransitionAnimation); assertNoStartingWindow(activity1); assertHasStartingWindow(activity2); } @@ -2965,7 +2964,6 @@ public class ActivityRecordTests extends WindowTestsBase { false /* newTask */, false /* isTaskSwitch */, null /* options */, null /* sourceRecord */); - assertTrue(mDisplayContent.mSkipAppTransitionAnimation); assertNull(middle.mStartingWindow); assertHasStartingWindow(top); assertTrue(top.isVisible()); @@ -3265,26 +3263,6 @@ public class ActivityRecordTests extends WindowTestsBase { > activity.getConfiguration().windowConfiguration.getAppBounds().height()); } - @Test - public void testSetVisibility_visibleToVisible() { - final ActivityRecord activity = new ActivityBuilder(mAtm) - .setCreateTask(true).build(); - // By default, activity is visible. - assertTrue(activity.isVisible()); - assertTrue(activity.isVisibleRequested()); - assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); - - // Request the activity to be visible. Although the activity is already visible, app - // transition animation should be applied on this activity. This might be unnecessary, but - // until we verify no logic relies on this behavior, we'll keep this as is. - mDisplayContent.prepareAppTransition(0); - activity.setVisibility(true); - assertTrue(activity.isVisible()); - assertTrue(activity.isVisibleRequested()); - assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity)); - assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); - } - @SetupWindows(addWindows = W_ACTIVITY) @Test public void testSetVisibility_visibleToInvisible() { @@ -3316,50 +3294,30 @@ public class ActivityRecordTests extends WindowTestsBase { public void testSetVisibility_invisibleToVisible() { final ActivityRecord activity = new ActivityBuilder(mAtm) .setCreateTask(true).setVisible(false).build(); - // Activiby is invisible. However ATMS requests it to become visible, since this is a top - // activity. assertFalse(activity.isVisible()); - assertTrue(activity.isVisibleRequested()); - assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); + assertFalse(activity.isVisibleRequested()); // Request the activity to be visible. Since the visibility changes, app transition // animation should be applied on this activity. - activity.setVisibility(true); + requestTransition(activity, WindowManager.TRANSIT_OPEN); + mWm.mRoot.resumeFocusedTasksTopActivities(); assertFalse(activity.isVisible()); assertTrue(activity.isVisibleRequested()); - assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity)); - assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); - - // There should still be animation (add to opening) if keyguard is going away while the - // screen is off because it will be visible after screen is turned on by unlocking. - mDisplayContent.mOpeningApps.remove(activity); - mDisplayContent.mClosingApps.remove(activity); - activity.commitVisibility(false /* visible */, false /* performLayout */); - mDisplayContent.getDisplayPolicy().screenTurnedOff(false /* acquireSleepToken */); - final KeyguardController controller = mSupervisor.getKeyguardController(); - doReturn(true).when(controller).isKeyguardGoingAway(anyInt()); - activity.setVisibility(true); - assertTrue(mDisplayContent.mOpeningApps.contains(activity)); + assertTrue(activity.inTransition()); } @Test public void testSetVisibility_invisibleToInvisible() { final ActivityRecord activity = new ActivityBuilder(mAtm) .setCreateTask(true).setVisible(false).build(); - // Activiby is invisible. However ATMS requests it to become visible, since this is a top - // activity. - assertFalse(activity.isVisible()); - assertTrue(activity.isVisibleRequested()); - assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity)); - assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); + requestTransition(activity, WindowManager.TRANSIT_CLOSE); // Request the activity to be invisible. Since the activity is already invisible, no app // transition should be applied on this activity. activity.setVisibility(false); assertFalse(activity.isVisible()); assertFalse(activity.isVisibleRequested()); - assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity)); - assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); + assertFalse(activity.inTransition()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/AnimatingActivityRegistryTest.java b/services/tests/wmtests/src/com/android/server/wm/AnimatingActivityRegistryTest.java deleted file mode 100644 index 8871056988ef..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/AnimatingActivityRegistryTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.platform.test.annotations.Presubmit; - -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Tests for the {@link ActivityStack} class. - * - * Build/Install/Run: - * atest WmTests:AnimatingActivityRegistryTest - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class AnimatingActivityRegistryTest extends WindowTestsBase { - - @Mock - AnimationAdapter mAdapter; - - @Mock - Runnable mMockEndDeferFinishCallback1; - @Mock - Runnable mMockEndDeferFinishCallback2; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testDeferring() { - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - final ActivityRecord activity2 = createAppWindow(activity1.getTask(), ACTIVITY_TYPE_STANDARD, - "activity2").mActivityRecord; - final AnimatingActivityRegistry registry = - activity1.getRootTask().getAnimatingActivityRegistry(); - - activity1.startAnimation(activity1.getPendingTransaction(), mAdapter, false /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - activity2.startAnimation(activity1.getPendingTransaction(), mAdapter, false /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - assertTrue(activity1.isAnimating(TRANSITION)); - assertTrue(activity2.isAnimating(TRANSITION)); - - // Make sure that first animation finish is deferred, second one is not deferred, and first - // one gets cancelled. - assertTrue(registry.notifyAboutToFinish(activity1, mMockEndDeferFinishCallback1)); - assertFalse(registry.notifyAboutToFinish(activity2, mMockEndDeferFinishCallback2)); - verify(mMockEndDeferFinishCallback1).run(); - verifyZeroInteractions(mMockEndDeferFinishCallback2); - } - - @Test - public void testContainerRemoved() { - final ActivityRecord window1 = createActivityRecord(mDisplayContent); - final ActivityRecord window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD, - "window2").mActivityRecord; - final AnimatingActivityRegistry registry = - window1.getRootTask().getAnimatingActivityRegistry(); - - window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - assertTrue(window1.isAnimating(TRANSITION)); - assertTrue(window2.isAnimating(TRANSITION)); - - // Make sure that first animation finish is deferred, and removing the second window stops - // finishes all pending deferred finishings. - registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1); - window2.setParent(null); - verify(mMockEndDeferFinishCallback1).run(); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java deleted file mode 100644 index c294bc62c7ac..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ /dev/null @@ -1,1306 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_TO_FRONT; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.annotation.Nullable; -import android.graphics.Rect; -import android.gui.DropInputMode; -import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; -import android.platform.test.annotations.Presubmit; -import android.util.ArraySet; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationDefinition; -import android.view.RemoteAnimationTarget; -import android.view.WindowManager; -import android.window.ITaskFragmentOrganizer; -import android.window.TaskFragmentOrganizer; - -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Build/Install/Run: - * atest WmTests:AppTransitionControllerTest - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class AppTransitionControllerTest extends WindowTestsBase { - - private AppTransitionController mAppTransitionController; - - @Before - public void setUp() throws Exception { - assumeFalse(WindowManagerService.sEnableShellTransitions); - mAppTransitionController = new AppTransitionController(mWm, mDisplayContent); - mWm.mAnimator.ready(); - } - - @Test - public void testSkipOccludedActivityCloseTransition() { - final ActivityRecord behind = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final ActivityRecord topOpening = createActivityRecord(behind.getTask()); - topOpening.setOccludesParent(true); - topOpening.setVisible(true); - - mDisplayContent.prepareAppTransition(TRANSIT_OPEN); - mDisplayContent.prepareAppTransition(TRANSIT_CLOSE); - mDisplayContent.mClosingApps.add(behind); - - assertEquals(WindowManager.TRANSIT_OLD_UNSET, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null, null, false)); - } - - @Test - public void testClearTaskSkipAppExecuteTransition() { - final ActivityRecord behind = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final Task task = behind.getTask(); - final ActivityRecord top = createActivityRecord(task); - top.setState(ActivityRecord.State.RESUMED, "test"); - behind.setState(ActivityRecord.State.STARTED, "test"); - behind.setVisibleRequested(true); - - task.removeActivities("test", false /* excludingTaskOverlay */); - assertFalse(mDisplayContent.mAppTransition.isReady()); - } - - @Test - public void testTranslucentOpen() { - final ActivityRecord behind = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final ActivityRecord translucentOpening = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - doReturn(false).when(translucentOpening).fillsParent(); - translucentOpening.setVisible(false); - mDisplayContent.prepareAppTransition(TRANSIT_OPEN); - mDisplayContent.mOpeningApps.add(behind); - mDisplayContent.mOpeningApps.add(translucentOpening); - - assertEquals(WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null, null, false)); - } - - @Test - public void testTranslucentClose() { - final ActivityRecord behind = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final ActivityRecord translucentClosing = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - doReturn(false).when(translucentClosing).fillsParent(); - mDisplayContent.prepareAppTransition(TRANSIT_CLOSE); - mDisplayContent.mClosingApps.add(translucentClosing); - assertEquals(WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null, null, false)); - } - - @Test - public void testDreamActivityOpenTransition() { - final ActivityRecord dreamActivity = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM); - mDisplayContent.prepareAppTransition(TRANSIT_OPEN); - mDisplayContent.mOpeningApps.add(dreamActivity); - - assertEquals(TRANSIT_OLD_DREAM_ACTIVITY_OPEN, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null, null, false)); - } - - @Test - public void testDreamActivityCloseTransition() { - final ActivityRecord dreamActivity = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM); - mDisplayContent.prepareAppTransition(TRANSIT_CLOSE); - mDisplayContent.mClosingApps.add(dreamActivity); - - assertEquals(TRANSIT_OLD_DREAM_ACTIVITY_CLOSE, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null, null, false)); - } - - @Test - public void testChangeIsNotOverwritten() { - final ActivityRecord behind = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final ActivityRecord translucentOpening = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - translucentOpening.setOccludesParent(false); - translucentOpening.setVisible(false); - mDisplayContent.prepareAppTransition(TRANSIT_CHANGE); - mDisplayContent.mOpeningApps.add(behind); - mDisplayContent.mOpeningApps.add(translucentOpening); - mDisplayContent.mChangingContainers.add(translucentOpening.getTask()); - assertEquals(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null, null, false)); - } - - @Test - public void testTransitWithinTask() { - final ActivityRecord opening = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD); - opening.setOccludesParent(false); - final ActivityRecord closing = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD); - closing.setOccludesParent(false); - final Task task = opening.getTask(); - mDisplayContent.mOpeningApps.add(opening); - mDisplayContent.mClosingApps.add(closing); - assertFalse(mAppTransitionController.isTransitWithinTask(TRANSIT_OLD_ACTIVITY_OPEN, task)); - closing.getTask().removeChild(closing); - task.addChild(closing, 0); - assertTrue(mAppTransitionController.isTransitWithinTask(TRANSIT_OLD_ACTIVITY_OPEN, task)); - assertFalse(mAppTransitionController.isTransitWithinTask(TRANSIT_OLD_TASK_OPEN, task)); - } - - - @Test - public void testIntraWallpaper_open() { - final ActivityRecord opening = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - opening.setVisible(false); - final WindowManager.LayoutParams attrOpening = new WindowManager.LayoutParams( - TYPE_BASE_APPLICATION); - attrOpening.setTitle("WallpaperOpening"); - attrOpening.flags |= FLAG_SHOW_WALLPAPER; - final TestWindowState appWindowOpening = createWindowState(attrOpening, opening); - opening.addWindow(appWindowOpening); - - final ActivityRecord closing = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final WindowManager.LayoutParams attrClosing = new WindowManager.LayoutParams( - TYPE_BASE_APPLICATION); - attrOpening.setTitle("WallpaperClosing"); - attrClosing.flags |= FLAG_SHOW_WALLPAPER; - final TestWindowState appWindowClosing = createWindowState(attrClosing, closing); - closing.addWindow(appWindowClosing); - - mDisplayContent.prepareAppTransition(TRANSIT_OPEN); - mDisplayContent.mOpeningApps.add(opening); - mDisplayContent.mClosingApps.add(closing); - - assertEquals(WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, appWindowClosing, null, false)); - } - - @Test - public void testIntraWallpaper_toFront() { - final ActivityRecord opening = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - opening.setVisible(false); - final WindowManager.LayoutParams attrOpening = new WindowManager.LayoutParams( - TYPE_BASE_APPLICATION); - attrOpening.setTitle("WallpaperOpening"); - attrOpening.flags |= FLAG_SHOW_WALLPAPER; - final TestWindowState appWindowOpening = createWindowState(attrOpening, opening); - opening.addWindow(appWindowOpening); - - final ActivityRecord closing = createActivityRecord(mDisplayContent, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); - final WindowManager.LayoutParams attrClosing = new WindowManager.LayoutParams( - TYPE_BASE_APPLICATION); - attrOpening.setTitle("WallpaperClosing"); - attrClosing.flags |= FLAG_SHOW_WALLPAPER; - final TestWindowState appWindowClosing = createWindowState(attrClosing, closing); - closing.addWindow(appWindowClosing); - - mDisplayContent.prepareAppTransition(TRANSIT_TO_FRONT); - mDisplayContent.mOpeningApps.add(opening); - mDisplayContent.mClosingApps.add(closing); - - assertEquals(WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, appWindowClosing, null, false)); - } - - @Test - public void testGetAnimationTargets_visibilityAlreadyUpdated() { - // [DisplayContent] -+- [Task1] - [ActivityRecord1] (opening, visible) - // +- [Task2] - [ActivityRecord2] (closing, invisible) - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - - final ActivityRecord activity2 = createActivityRecord(mDisplayContent); - activity2.setVisible(false); - activity2.setVisibleRequested(false); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // No animation, since visibility of the opening and closing apps are already updated - // outside of AppTransition framework. - assertEquals( - new ArraySet<>(), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_visibilityAlreadyUpdated_butForcedTransitionRequested() { - // [DisplayContent] -+- [Task1] - [ActivityRecord1] (closing, invisible) - // +- [Task2] - [ActivityRecord2] (opening, visible) - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - activity1.setVisible(true); - activity1.setVisibleRequested(true); - activity1.mRequestForceTransition = true; - - final ActivityRecord activity2 = createActivityRecord(mDisplayContent); - activity2.setVisible(false); - activity2.setVisibleRequested(false); - activity2.mRequestForceTransition = true; - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // The visibility are already updated, but since forced transition is requested, it will - // be included. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity2.getRootTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_exitingBeforeTransition() { - // Create another non-empty task so the animation target won't promote to task display area. - createActivityRecord(mDisplayContent); - final ActivityRecord activity = createActivityRecord(mDisplayContent); - activity.setVisible(false); - activity.mIsExiting = true; - - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity); - - // Animate closing apps even if it's not visible when it is exiting before we had a chance - // to play the transition animation. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity.getRootTask()}), - AppTransitionController.getAnimationTargets( - new ArraySet<>(), closing, false /* visible */)); - } - - @Test - public void testExitAnimationDone_beforeAppTransition() { - final Task task = createTask(mDisplayContent); - final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "Win"); - spyOn(win); - win.mAnimatingExit = true; - mDisplayContent.mAppTransition.setTimeout(); - mDisplayContent.mAppTransitionController.handleAppTransitionReady(); - - verify(win).onExitAnimationDone(); - } - - @Test - public void testGetAnimationTargets_openingClosingInDifferentTask() { - // [DisplayContent] -+- [Task1] -+- [ActivityRecord1] (opening, invisible) - // | +- [ActivityRecord2] (invisible) - // | - // +- [Task2] -+- [ActivityRecord3] (closing, visible) - // +- [ActivityRecord4] (invisible) - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - final ActivityRecord activity2 = createActivityRecord(mDisplayContent, - activity1.getTask()); - activity2.setVisible(false); - activity2.setVisibleRequested(false); - - final ActivityRecord activity3 = createActivityRecord(mDisplayContent); - final ActivityRecord activity4 = createActivityRecord(mDisplayContent, - activity3.getTask()); - activity4.setVisible(false); - activity4.setVisibleRequested(false); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity3); - - // Promote animation targets to root Task level. Invisible ActivityRecords don't affect - // promotion decision. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity3.getRootTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_openingClosingInSameTask() { - // [DisplayContent] - [Task] -+- [ActivityRecord1] (opening, invisible) - // +- [ActivityRecord2] (closing, visible) - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - final ActivityRecord activity2 = createActivityRecord(mDisplayContent, - activity1.getTask()); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // Don't promote an animation target to Task level, since the same task contains both - // opening and closing app. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity2}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_animateOnlyTranslucentApp() { - // [DisplayContent] -+- [Task1] -+- [ActivityRecord1] (opening, invisible) - // | +- [ActivityRecord2] (visible) - // | - // +- [Task2] -+- [ActivityRecord3] (closing, visible) - // +- [ActivityRecord4] (visible) - - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - activity1.setOccludesParent(false); - - final ActivityRecord activity2 = createActivityRecord(mDisplayContent, - activity1.getTask()); - - final ActivityRecord activity3 = createActivityRecord(mDisplayContent); - activity3.setOccludesParent(false); - final ActivityRecord activity4 = createActivityRecord(mDisplayContent, - activity3.getTask()); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity3); - - // Don't promote an animation target to Task level, since opening (closing) app is - // translucent and is displayed over other non-animating app. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity3}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_animateTranslucentAndOpaqueApps() { - // [DisplayContent] -+- [Task1] -+- [ActivityRecord1] (opening, invisible) - // | +- [ActivityRecord2] (opening, invisible) - // | - // +- [Task2] -+- [ActivityRecord3] (closing, visible) - // +- [ActivityRecord4] (closing, visible) - - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - activity1.setOccludesParent(false); - - final ActivityRecord activity2 = createActivityRecord(mDisplayContent, - activity1.getTask()); - activity2.setVisible(false); - activity2.setVisibleRequested(true); - - final ActivityRecord activity3 = createActivityRecord(mDisplayContent); - activity3.setOccludesParent(false); - final ActivityRecord activity4 = createActivityRecord(mDisplayContent, - activity3.getTask()); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - opening.add(activity2); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity3); - closing.add(activity4); - - // Promote animation targets to TaskStack level even though opening (closing) app is - // translucent as long as all visible siblings animate at the same time. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1.getRootTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity3.getRootTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_taskContainsMultipleTasks() { - // [DisplayContent] - [Task] -+- [Task1] - [ActivityRecord1] (opening, invisible) - // +- [Task2] - [ActivityRecord2] (closing, visible) - final Task parentTask = createTask(mDisplayContent); - final ActivityRecord activity1 = createActivityRecordWithParentTask(parentTask); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - final ActivityRecord activity2 = createActivityRecordWithParentTask(parentTask); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // Promote animation targets up to Task level, not beyond. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1.getTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity2.getTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_splitScreenOpening() { - // [DisplayContent] - [Task] -+- [split task 1] -+- [Task1] - [AR1] (opening, invisible) - // +- [split task 2] -+- [Task2] - [AR2] (opening, invisible) - final Task singleTopRoot = createTask(mDisplayContent); - final TaskBuilder builder = new TaskBuilder(mSupervisor) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .setParentTask(singleTopRoot) - .setCreatedByOrganizer(true); - final Task splitRoot1 = builder.build(); - final Task splitRoot2 = builder.build(); - splitRoot1.setAdjacentTaskFragment(splitRoot2); - final ActivityRecord activity1 = createActivityRecordWithParentTask(splitRoot1); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - final ActivityRecord activity2 = createActivityRecordWithParentTask(splitRoot2); - activity2.setVisible(false); - activity2.setVisibleRequested(true); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - opening.add(activity2); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - - // Promote animation targets up to Task level, not beyond. - assertEquals( - new ArraySet<>(new WindowContainer[]{splitRoot1, splitRoot2}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - } - - @Test - public void testGetAnimationTargets_openingClosingTaskFragment() { - // [DefaultTDA] - [Task] -+- [TaskFragment1] - [ActivityRecord1] (opening, invisible) - // +- [TaskFragment2] - [ActivityRecord2] (closing, visible) - final Task parentTask = createTask(mDisplayContent); - final TaskFragment taskFragment1 = createTaskFragmentWithActivity(parentTask); - final ActivityRecord activity1 = taskFragment1.getTopMostActivity(); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - - final TaskFragment taskFragment2 = createTaskFragmentWithActivity(parentTask); - final ActivityRecord activity2 = taskFragment2.getTopMostActivity(); - activity2.setVisible(true); - activity2.setVisibleRequested(false); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // Promote animation targets up to TaskFragment level, not beyond. - assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment1}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals(new ArraySet<>(new WindowContainer[]{taskFragment2}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_openingTheOnlyTaskFragmentInTask() { - // [DefaultTDA] -+- [Task1] - [TaskFragment1] - [ActivityRecord1] (opening, invisible) - // +- [Task2] - [ActivityRecord2] (closing, visible) - final Task task1 = createTask(mDisplayContent); - final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1); - final ActivityRecord activity1 = taskFragment1.getTopMostActivity(); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - - final ActivityRecord activity2 = createActivityRecord(mDisplayContent); - activity2.setVisible(true); - activity2.setVisibleRequested(false); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // Promote animation targets up to leaf Task level because there's only one TaskFragment in - // the Task. - assertEquals(new ArraySet<>(new WindowContainer[]{task1}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals(new ArraySet<>(new WindowContainer[]{activity2.getTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_closingTheOnlyTaskFragmentInTask() { - // [DefaultTDA] -+- [Task1] - [TaskFragment1] - [ActivityRecord1] (closing, visible) - // +- [Task2] - [ActivityRecord2] (opening, invisible) - final Task task1 = createTask(mDisplayContent); - final TaskFragment taskFragment1 = createTaskFragmentWithActivity(task1); - final ActivityRecord activity1 = taskFragment1.getTopMostActivity(); - activity1.setVisible(true); - activity1.setVisibleRequested(false); - - final ActivityRecord activity2 = createActivityRecord(mDisplayContent); - activity2.setVisible(false); - activity2.setVisibleRequested(true); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity2); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity1); - - // Promote animation targets up to leaf Task level because there's only one TaskFragment in - // the Task. - assertEquals(new ArraySet<>(new WindowContainer[]{activity2.getTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals(new ArraySet<>(new WindowContainer[]{task1}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - @Test - public void testGetAnimationTargets_embeddedTask() { - // [DisplayContent] -+- [Task1] - [ActivityRecord1] (opening, invisible) - // +- [Task2] (embedded) - [ActivityRecord2] (opening, invisible) - final ActivityRecord activity1 = createActivityRecord(mDisplayContent); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - - final Task task2 = createTask(mDisplayContent); - task2.mRemoveWithTaskOrganizer = true; - final ActivityRecord activity2 = createActivityRecord(task2); - activity2.setVisible(false); - activity2.setVisibleRequested(true); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - opening.add(activity2); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - - // No animation on the embedded task. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1.getTask()}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - - @Test - public void testGetAnimationTargets_activityInEmbeddedTask() { - // [DisplayContent] - [Task] (embedded)-+- [ActivityRecord1] (opening, invisible) - // +- [ActivityRecord2] (closing, visible) - final Task task = createTask(mDisplayContent); - task.mRemoveWithTaskOrganizer = true; - - final ActivityRecord activity1 = createActivityRecord(task); - activity1.setVisible(false); - activity1.setVisibleRequested(true); - final ActivityRecord activity2 = createActivityRecord(task); - - final ArraySet<ActivityRecord> opening = new ArraySet<>(); - opening.add(activity1); - final ArraySet<ActivityRecord> closing = new ArraySet<>(); - closing.add(activity2); - - // Even though embedded task itself doesn't animate, activities in an embedded task - // animate. - assertEquals( - new ArraySet<>(new WindowContainer[]{activity1}), - AppTransitionController.getAnimationTargets( - opening, closing, true /* visible */)); - assertEquals( - new ArraySet<>(new WindowContainer[]{activity2}), - AppTransitionController.getAnimationTargets( - opening, closing, false /* visible */)); - } - - static class TestRemoteAnimationRunner implements IRemoteAnimationRunner { - private IRemoteAnimationFinishedCallback mFinishedCallback; - - @Override - public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { - mFinishedCallback = finishedCallback; - } - - @Override - public void onAnimationCancelled() throws RemoteException { - mFinishedCallback = null; - } - - @Override - public IBinder asBinder() { - return new Binder(); - } - - boolean isAnimationStarted() { - return mFinishedCallback != null; - } - - void finishAnimation() { - try { - mFinishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - fail(); - } - } - } - - @Test - public void testGetRemoteAnimationOverrideEmpty() { - final ActivityRecord activity = createActivityRecord(mDisplayContent); - assertNull(mAppTransitionController.getRemoteAnimationOverride(activity, - TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>())); - } - - @Test - public void testGetRemoteAnimationOverrideWindowContainer() { - final ActivityRecord activity = createActivityRecord(mDisplayContent); - final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - new TestRemoteAnimationRunner(), 10, 1); - definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter); - activity.registerRemoteAnimations(definition); - - assertEquals(adapter, - mAppTransitionController.getRemoteAnimationOverride( - activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>())); - assertNull(mAppTransitionController.getRemoteAnimationOverride( - null, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>())); - } - - @Test - public void testGetRemoteAnimationOverrideTransitionController() { - final ActivityRecord activity = createActivityRecord(mDisplayContent); - final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - new TestRemoteAnimationRunner(), 10, 1); - definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter); - mAppTransitionController.registerRemoteAnimations(definition); - - assertEquals(adapter, - mAppTransitionController.getRemoteAnimationOverride( - activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>())); - assertEquals(adapter, - mAppTransitionController.getRemoteAnimationOverride( - null, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>())); - } - - @Test - public void testGetRemoteAnimationOverrideBoth() { - final ActivityRecord activity = createActivityRecord(mDisplayContent); - final RemoteAnimationDefinition definition1 = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter adapter1 = new RemoteAnimationAdapter( - new TestRemoteAnimationRunner(), 10, 1); - definition1.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter1); - activity.registerRemoteAnimations(definition1); - - final RemoteAnimationDefinition definition2 = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter adapter2 = new RemoteAnimationAdapter( - new TestRemoteAnimationRunner(), 10, 1); - definition2.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_UNOCCLUDE, adapter2); - mAppTransitionController.registerRemoteAnimations(definition2); - - assertEquals(adapter2, - mAppTransitionController.getRemoteAnimationOverride( - activity, TRANSIT_OLD_KEYGUARD_UNOCCLUDE, new ArraySet<Integer>())); - assertEquals(adapter2, - mAppTransitionController.getRemoteAnimationOverride( - null, TRANSIT_OLD_KEYGUARD_UNOCCLUDE, new ArraySet<Integer>())); - } - - @Test - public void testGetRemoteAnimationOverrideWindowContainerHasPriority() { - final ActivityRecord activity = createActivityRecord(mDisplayContent); - final RemoteAnimationDefinition definition1 = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter adapter1 = new RemoteAnimationAdapter( - new TestRemoteAnimationRunner(), 10, 1); - definition1.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter1); - activity.registerRemoteAnimations(definition1); - - final RemoteAnimationDefinition definition2 = new RemoteAnimationDefinition(); - final RemoteAnimationAdapter adapter2 = new RemoteAnimationAdapter( - new TestRemoteAnimationRunner(), 10, 1); - definition2.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter2); - mAppTransitionController.registerRemoteAnimations(definition2); - - assertEquals(adapter1, - mAppTransitionController.getRemoteAnimationOverride( - activity, TRANSIT_OLD_ACTIVITY_OPEN, new ArraySet<Integer>())); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideWithEmbeddedActivity() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord activity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(activity); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideWithOnlyTaskFragmentFillingTask() { - final Task task = createTask(mDisplayContent); - final ActivityRecord closingActivity = createActivityRecord(task); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - - // Make sure the TaskFragment is not embedded. - assertFalse(taskFragment.isEmbeddedWithBoundsOverride()); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - prepareActivityForAppTransition(openingActivity); - final int uid = 12345; - closingActivity.info.applicationInfo.uid = uid; - openingActivity.info.applicationInfo.uid = uid; - task.effectiveUid = uid; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, - null /* changingTaskFragment */); - waitUntilWindowAnimatorIdle(); - - // Animation is not run by the remote handler because the activity is filling the Task. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideWithTaskFragmentNotFillingTask() { - final Task task = createTask(mDisplayContent); - final ActivityRecord closingActivity = createActivityRecord(task); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - - // Make sure the TaskFragment is embedded. - taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - final Rect embeddedBounds = new Rect(task.getBounds()); - embeddedBounds.right = embeddedBounds.left + embeddedBounds.width() / 2; - taskFragment.setBounds(embeddedBounds); - assertTrue(taskFragment.isEmbeddedWithBoundsOverride()); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - prepareActivityForAppTransition(openingActivity); - final int uid = 12345; - closingActivity.info.applicationInfo.uid = uid; - openingActivity.info.applicationInfo.uid = uid; - task.effectiveUid = uid; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, - null /* changingTaskFragment */); - waitUntilWindowAnimatorIdle(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideWithNonEmbeddedActivity() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing non-embedded activity. - final ActivityRecord closingActivity = createActivityRecord(task); - prepareActivityForAppTransition(closingActivity); - // Opening TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(openingActivity); - task.effectiveUid = openingActivity.getUid(); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - waitUntilWindowAnimatorIdle(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_overrideEmbeddedActivityWithDiffUid() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing TaskFragment with embedded activity. - final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord closingActivity = taskFragment1.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - closingActivity.info.applicationInfo.uid = 12345; - // Opening TaskFragment with embedded activity with different UID. - final TaskFragment taskFragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord openingActivity = taskFragment2.getTopMostActivity(); - prepareActivityForAppTransition(openingActivity); - openingActivity.info.applicationInfo.uid = 54321; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1); - waitUntilWindowAnimatorIdle(); - - // Animation run by the remote handler. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideWithTwoApps() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing activity in Task1. - final ActivityRecord closingActivity = createActivityRecord(mDisplayContent); - prepareActivityForAppTransition(closingActivity); - // Opening TaskFragment with embedded activity in Task2. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord openingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(openingActivity); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - waitUntilWindowAnimatorIdle(); - - // Animation not run by the remote handler. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideNonEmbeddedActivityWithDiffUid() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Closing TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord closingActivity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(closingActivity); - closingActivity.info.applicationInfo.uid = 12345; - task.effectiveUid = closingActivity.getUid(); - // Opening non-embedded activity with different UID. - final ActivityRecord openingActivity = createActivityRecord(task); - prepareActivityForAppTransition(openingActivity); - openingActivity.info.applicationInfo.uid = 54321; - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - waitUntilWindowAnimatorIdle(); - - // Animation should not run by the remote handler when there are non-embedded activities of - // different UID. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_noOverrideWithWallpaper() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activity. - final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer); - final ActivityRecord activity = taskFragment.getTopMostActivity(); - prepareActivityForAppTransition(activity); - // Set wallpaper as visible. - final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, - mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */); - spyOn(mDisplayContent.mWallpaperController); - doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); - - // Animation should not run by the remote handler when there is wallpaper in the transition. - assertFalse(remoteAnimationRunner.isAnimationStarted()); - } - - @Test - public void testOverrideTaskFragmentAdapter_inputProtectedForUntrustedAnimation() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with embedded activities, one is trusted embedded, and the other - // one is untrusted embedded. - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(2) - .setOrganizer(organizer) - .build(); - final ActivityRecord activity0 = taskFragment.getChildAt(0).asActivityRecord(); - final ActivityRecord activity1 = taskFragment.getChildAt(1).asActivityRecord(); - // Also create a non-embedded activity in the Task. - final ActivityRecord activity2 = new ActivityBuilder(mAtm).build(); - task.addChild(activity2, POSITION_BOTTOM); - prepareActivityForAppTransition(activity0); - prepareActivityForAppTransition(activity1); - prepareActivityForAppTransition(activity2); - doReturn(false).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity0); - doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity1); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity1, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); - - // The animation will be animated remotely by client and all activities are input disabled - // for untrusted animation. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - verify(activity0).setDropInputForAnimation(true); - verify(activity1).setDropInputForAnimation(true); - verify(activity2).setDropInputForAnimation(true); - verify(activity0).setDropInputMode(DropInputMode.ALL); - verify(activity1).setDropInputMode(DropInputMode.ALL); - verify(activity2).setDropInputMode(DropInputMode.ALL); - - // Reset input after animation is finished. - clearInvocations(activity0); - clearInvocations(activity1); - clearInvocations(activity2); - remoteAnimationRunner.finishAnimation(); - - verify(activity0).setDropInputForAnimation(false); - verify(activity1).setDropInputForAnimation(false); - verify(activity2).setDropInputForAnimation(false); - verify(activity0).setDropInputMode(DropInputMode.OBSCURED); - verify(activity1).setDropInputMode(DropInputMode.NONE); - verify(activity2).setDropInputMode(DropInputMode.NONE); - } - - /** - * Since we don't have any use case to rely on handling input during animation, disable it even - * if it is trusted embedding so that it could cover some edge-cases when a previously trusted - * host starts doing something bad. - */ - @Test - public void testOverrideTaskFragmentAdapter_inputProtectedForTrustedAnimation() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with only trusted embedded activity - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setOrganizer(organizer) - .build(); - final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord(); - prepareActivityForAppTransition(activity); - doReturn(true).when(taskFragment).isAllowedToEmbedActivityInTrustedMode(activity); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); - - // The animation will be animated remotely by client and all activities are input disabled - // for untrusted animation. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - verify(activity).setDropInputForAnimation(true); - verify(activity).setDropInputMode(DropInputMode.ALL); - - // Reset input after animation is finished. - clearInvocations(activity); - remoteAnimationRunner.finishAnimation(); - - verify(activity).setDropInputForAnimation(false); - verify(activity).setDropInputMode(DropInputMode.NONE); - } - - /** - * We don't need to drop input for fully trusted embedding (system app, and embedding in the - * same app). This will allow users to do fast tapping. - */ - @Test - public void testOverrideTaskFragmentAdapter_noInputProtectedForFullyTrustedAnimation() { - final Task task = createTask(mDisplayContent); - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); - setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner); - - // Create a TaskFragment with only trusted embedded activity - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setOrganizer(organizer) - .build(); - final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord(); - prepareActivityForAppTransition(activity); - final int uid = mAtm.mTaskFragmentOrganizerController.getTaskFragmentOrganizerUid( - getITaskFragmentOrganizer(organizer)); - doReturn(true).when(task).isFullyTrustedEmbedding(uid); - spyOn(mDisplayContent.mAppTransition); - - // Prepare and start transition. - prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); - - // The animation will be animated remotely by client, but input should not be dropped for - // fully trusted. - assertTrue(remoteAnimationRunner.isAnimationStarted()); - verify(activity, never()).setDropInputForAnimation(true); - verify(activity, never()).setDropInputMode(DropInputMode.ALL); - } - - @Test - public void testTransitionGoodToGoForTaskFragments() { - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final Task task = createTask(mDisplayContent); - final TaskFragment changeTaskFragment = - createTaskFragmentWithEmbeddedActivity(task, organizer); - final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .setOrganizer(organizer) - .build(); - prepareActivityForAppTransition(changeTaskFragment.getTopMostActivity()); - spyOn(mDisplayContent.mAppTransition); - spyOn(emptyTaskFragment); - - prepareAndTriggerAppTransition( - null /* openingActivity */, null /* closingActivity*/, changeTaskFragment); - - // Transition not ready because there is an empty non-finishing TaskFragment. - verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any()); - - doReturn(true).when(emptyTaskFragment).hasChild(); - emptyTaskFragment.remove(false /* withTransition */, "test"); - - mDisplayContent.mAppTransitionController.handleAppTransitionReady(); - - // Transition ready because the empty (no running activity) TaskFragment is requested to be - // removed. - verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any()); - } - - @Test - public void testTransitionGoodToGoForTaskFragments_detachedApp() { - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer); - registerTaskFragmentOrganizer(iOrganizer); - final Task task = createTask(mDisplayContent); - final TaskFragment changeTaskFragment = - createTaskFragmentWithEmbeddedActivity(task, organizer); - final TaskFragment emptyTaskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .setOrganizer(organizer) - .build(); - prepareActivityForAppTransition(changeTaskFragment.getTopMostActivity()); - // To make sure that having a detached activity won't cause any issue. - final ActivityRecord detachedActivity = createActivityRecord(task); - detachedActivity.removeImmediately(); - assertNull(detachedActivity.getRootTask()); - spyOn(mDisplayContent.mAppTransition); - spyOn(emptyTaskFragment); - - prepareAndTriggerAppTransition( - null /* openingActivity */, detachedActivity, changeTaskFragment); - - // Transition not ready because there is an empty non-finishing TaskFragment. - verify(mDisplayContent.mAppTransition, never()).goodToGo(anyInt(), any()); - - doReturn(true).when(emptyTaskFragment).hasChild(); - emptyTaskFragment.remove(false /* withTransition */, "test"); - - mDisplayContent.mAppTransitionController.handleAppTransitionReady(); - - // Transition ready because the empty (no running activity) TaskFragment is requested to be - // removed. - verify(mDisplayContent.mAppTransition).goodToGo(anyInt(), any()); - } - - /** Registers remote animation for the organizer. */ - private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer, - TestRemoteAnimationRunner remoteAnimationRunner) { - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - remoteAnimationRunner, 10, 1); - final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer); - final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter); - definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, adapter); - registerTaskFragmentOrganizer(iOrganizer); - mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition); - } - - private static ITaskFragmentOrganizer getITaskFragmentOrganizer( - TaskFragmentOrganizer organizer) { - return ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); - } - - private void prepareAndTriggerAppTransition(@Nullable ActivityRecord openingActivity, - @Nullable ActivityRecord closingActivity, @Nullable TaskFragment changingTaskFragment) { - if (openingActivity != null) { - mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0); - mDisplayContent.mOpeningApps.add(openingActivity); - } - if (closingActivity != null) { - mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CLOSE, 0); - mDisplayContent.mClosingApps.add(closingActivity); - } - if (changingTaskFragment != null) { - mDisplayContent.mAppTransition.prepareAppTransition(TRANSIT_CHANGE, 0); - mDisplayContent.mChangingContainers.add(changingTaskFragment); - } - mDisplayContent.mAppTransitionController.handleAppTransitionReady(); - } - - private static void prepareActivityForAppTransition(ActivityRecord activity) { - // Transition will wait until all participated activities to be drawn. - activity.allDrawn = true; - // Skip manipulate the SurfaceControl. - doNothing().when(activity).setDropInputMode(anyInt()); - // Assume the activity contains a window. - doReturn(true).when(activity).hasChild(); - // Make sure activity can create remote animation target. - doReturn(mock(RemoteAnimationTarget.class)).when(activity).createRemoteAnimationTarget( - any()); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java deleted file mode 100644 index 8553fbd30ab8..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; -import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_UNSET; -import static android.view.WindowManager.TRANSIT_OPEN; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.server.wm.WindowContainer.POSITION_TOP; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; - -import android.graphics.Rect; -import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; -import android.platform.test.annotations.Presubmit; -import android.util.ArraySet; -import android.view.Display; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.WindowManager; -import android.view.animation.Animation; -import android.window.ITaskFragmentOrganizer; -import android.window.TaskFragmentOrganizer; - -import androidx.test.filters.SmallTest; - -import com.android.internal.policy.TransitionAnimation; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test class for {@link AppTransition}. - * - * Build/Install/Run: - * atest WmTests:AppTransitionTests - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class AppTransitionTests extends WindowTestsBase { - private DisplayContent mDc; - - @Before - public void setUp() throws Exception { - doNothing().when(mWm.mRoot).performSurfacePlacement(); - mDc = mWm.getDefaultDisplayContentLocked(); - } - - @Test - public void testKeyguardOverride() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - final ActivityRecord activity = createActivityRecord(dc); - - mDc.prepareAppTransition(TRANSIT_OPEN); - mDc.prepareAppTransition(TRANSIT_KEYGUARD_OCCLUDE); - mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY); - mDc.mOpeningApps.add(activity); - assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - } - - @Test - public void testKeyguardUnoccludeOcclude() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - final ActivityRecord activity = createActivityRecord(dc); - - mDc.prepareAppTransition(TRANSIT_KEYGUARD_UNOCCLUDE); - mDc.prepareAppTransition(TRANSIT_KEYGUARD_OCCLUDE); - mDc.mOpeningApps.add(activity); - assertEquals(TRANSIT_NONE, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - - } - - @Test - public void testKeyguardKeep() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - final ActivityRecord activity = createActivityRecord(dc); - - mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY); - mDc.prepareAppTransition(TRANSIT_OPEN); - mDc.mOpeningApps.add(activity); - assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - } - - @Test - public void testCrashing() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - final ActivityRecord activity = createActivityRecord(dc); - - mDc.prepareAppTransition(TRANSIT_OPEN); - mDc.prepareAppTransition(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED); - mDc.mClosingApps.add(activity); - assertEquals(TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - } - - @Test - public void testKeepKeyguard_withCrashing() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - final ActivityRecord activity = createActivityRecord(dc); - - mDc.prepareAppTransition(TRANSIT_KEYGUARD_GOING_AWAY); - mDc.prepareAppTransition(TRANSIT_CLOSE, TRANSIT_FLAG_APP_CRASHED); - mDc.mClosingApps.add(activity); - assertEquals(TRANSIT_OLD_KEYGUARD_GOING_AWAY, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - } - - @Test - public void testSkipTransitionAnimation() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - final ActivityRecord activity = createActivityRecord(dc); - - mDc.prepareAppTransition(TRANSIT_OPEN); - mDc.prepareAppTransition(TRANSIT_CLOSE); - mDc.mClosingApps.add(activity); - assertEquals(TRANSIT_OLD_UNSET, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, true /*skipAppTransitionAnimation*/)); - } - - @Test - public void testTaskChangeWindowingMode() { - final ActivityRecord activity = createActivityRecord(mDc); - - mDc.prepareAppTransition(TRANSIT_OPEN); - mDc.prepareAppTransition(TRANSIT_CHANGE); - mDc.mOpeningApps.add(activity); // Make sure TRANSIT_CHANGE has the priority - mDc.mChangingContainers.add(activity.getTask()); - - assertEquals(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - } - - @Test - public void testTaskFragmentChange() { - final ActivityRecord activity = createActivityRecord(mDc); - final TaskFragment taskFragment = new TaskFragment(mAtm, new Binder(), - true /* createdByOrganizer */, true /* isEmbedded */); - activity.getTask().addChild(taskFragment, POSITION_TOP); - activity.reparent(taskFragment, POSITION_TOP); - - mDc.prepareAppTransition(TRANSIT_OPEN); - mDc.prepareAppTransition(TRANSIT_CHANGE); - mDc.mOpeningApps.add(activity); // Make sure TRANSIT_CHANGE has the priority - mDc.mChangingContainers.add(taskFragment); - - assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, - AppTransitionController.getTransitCompatType(mDc.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /*skipAppTransitionAnimation*/)); - } - - @Test - public void testTaskFragmentOpeningTransition() { - final ActivityRecord activity = createHierarchyForTaskFragmentTest(); - activity.setVisible(false); - - mDisplayContent.prepareAppTransition(TRANSIT_OPEN); - mDisplayContent.mOpeningApps.add(activity); - assertEquals(TRANSIT_OLD_TASK_FRAGMENT_OPEN, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /* skipAppTransitionAnimation */)); - } - - @Test - public void testTaskFragmentClosingTransition() { - final ActivityRecord activity = createHierarchyForTaskFragmentTest(); - activity.setVisible(true); - - mDisplayContent.prepareAppTransition(TRANSIT_CLOSE); - mDisplayContent.mClosingApps.add(activity); - assertEquals(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, - AppTransitionController.getTransitCompatType(mDisplayContent.mAppTransition, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers, null /* wallpaperTarget */, - null /* oldWallpaper */, false /* skipAppTransitionAnimation */)); - } - - /** - * Creates a {@link Task} with two {@link TaskFragment TaskFragments}. - * The bottom TaskFragment is to prevent - * {@link AppTransitionController#getAnimationTargets(ArraySet, ArraySet, boolean) the animation - * target} to promote to Task or above. - * - * @return The Activity to be put in either opening or closing Activity - */ - private ActivityRecord createHierarchyForTaskFragmentTest() { - final Task parentTask = createTask(mDisplayContent); - final TaskFragment bottomTaskFragment = createTaskFragmentWithActivity(parentTask); - final ActivityRecord bottomActivity = bottomTaskFragment.getTopMostActivity(); - bottomActivity.setOccludesParent(true); - bottomActivity.setVisible(true); - - final TaskFragment verifiedTaskFragment = createTaskFragmentWithActivity(parentTask); - final ActivityRecord activity = verifiedTaskFragment.getTopMostActivity(); - activity.setOccludesParent(true); - - return activity; - } - - @Test - public void testAppTransitionStateForMultiDisplay() { - // Create 2 displays & presume both display the state is ON for ready to display & animate. - final DisplayContent dc1 = createNewDisplay(Display.STATE_ON); - final DisplayContent dc2 = createNewDisplay(Display.STATE_ON); - - // Create 2 app window tokens to represent 2 activity window. - final ActivityRecord activity1 = createActivityRecord(dc1); - final ActivityRecord activity2 = createActivityRecord(dc2); - - activity1.allDrawn = true; - activity1.startingMoved = true; - - // Simulate activity resume / finish flows to prepare app transition & set visibility, - // make sure transition is set as expected for each display. - dc1.prepareAppTransition(TRANSIT_OPEN); - dc2.prepareAppTransition(TRANSIT_CLOSE); - // One activity window is visible for resuming & the other activity window is invisible - // for finishing in different display. - activity1.setVisibility(true); - activity2.setVisibility(false); - - // Make sure each display is in animating stage. - assertTrue(dc1.mOpeningApps.size() > 0); - assertTrue(dc2.mClosingApps.size() > 0); - assertTrue(dc1.isAppTransitioning()); - assertTrue(dc2.isAppTransitioning()); - } - - @Test - public void testCleanAppTransitionWhenRootTaskReparent() { - // Create 2 displays & presume both display the state is ON for ready to display & animate. - final DisplayContent dc1 = createNewDisplay(Display.STATE_ON); - final DisplayContent dc2 = createNewDisplay(Display.STATE_ON); - - final Task rootTask1 = createTask(dc1); - final Task task1 = createTaskInRootTask(rootTask1, 0 /* userId */); - final ActivityRecord activity1 = createNonAttachedActivityRecord(dc1); - task1.addChild(activity1, 0); - - // Simulate same app is during opening / closing transition set stage. - dc1.mClosingApps.add(activity1); - assertTrue(dc1.mClosingApps.size() > 0); - - dc1.prepareAppTransition(TRANSIT_OPEN); - assertTrue(dc1.mAppTransition.containsTransitRequest(TRANSIT_OPEN)); - assertTrue(dc1.mAppTransition.isTransitionSet()); - - dc1.mOpeningApps.add(activity1); - assertTrue(dc1.mOpeningApps.size() > 0); - - // Move root task to another display. - rootTask1.reparent(dc2.getDefaultTaskDisplayArea(), true); - - // Verify if token are cleared from both pending transition list in former display. - assertFalse(dc1.mOpeningApps.contains(activity1)); - assertFalse(dc1.mOpeningApps.contains(activity1)); - } - - @Test - public void testLoadAnimationSafely() { - DisplayContent dc = createNewDisplay(Display.STATE_ON); - assertNull(dc.mAppTransition.loadAnimationSafely( - getInstrumentation().getTargetContext(), -1)); - } - - @Test - public void testCancelRemoteAnimationWhenFreeze() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - doReturn(false).when(dc).onDescendantOrientationChanged(any()); - final WindowState exitingAppWindow = newWindowBuilder("exiting app", - TYPE_BASE_APPLICATION).setDisplay(dc).build(); - final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord; - // Wait until everything in animation handler get executed to prevent the exiting window - // from being removed during WindowSurfacePlacer Traversal. - waitUntilHandlersIdle(); - - // Set a remote animator. - final TestRemoteAnimationRunner runner = new TestRemoteAnimationRunner(); - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - runner, 100, 50, true /* changeNeedsSnapshot */); - // RemoteAnimationController will tracking RemoteAnimationAdapter's caller with calling pid. - adapter.setCallingPidUid(123, 456); - - // Simulate activity finish flows to prepare app transition & set visibility, - // make sure transition is set as expected. - dc.prepareAppTransition(TRANSIT_CLOSE); - assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_CLOSE)); - dc.mAppTransition.overridePendingAppTransitionRemote(adapter); - exitingActivity.setVisibility(false); - assertTrue(dc.mClosingApps.size() > 0); - - // Make sure window is in animating stage before freeze, and cancel after freeze. - assertTrue(dc.isAppTransitioning()); - assertFalse(runner.mCancelled); - dc.mAppTransition.freeze(); - assertFalse(dc.isAppTransitioning()); - assertTrue(runner.mCancelled); - } - - @Test - public void testGetAnimationStyleResId() { - // Verify getAnimationStyleResId will return as LayoutParams.windowAnimations when without - // specifying window type. - final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(); - attrs.windowAnimations = 0x12345678; - assertEquals(attrs.windowAnimations, mDc.mAppTransition.getAnimationStyleResId(attrs)); - - // Verify getAnimationStyleResId will return system resource Id when the window type is - // starting window. - attrs.type = TYPE_APPLICATION_STARTING; - assertEquals(mDc.mAppTransition.getDefaultWindowAnimationStyleResId(), - mDc.mAppTransition.getAnimationStyleResId(attrs)); - } - - @Test - public void testActivityRecordReparentedToTaskFragment() { - final ActivityRecord activity = createActivityRecord(mDc); - final SurfaceControl activityLeash = mock(SurfaceControl.class); - doNothing().when(activity).setDropInputMode(anyInt()); - activity.setVisibility(true); - activity.setSurfaceControl(activityLeash); - final Task task = activity.getTask(); - - // Add a TaskFragment of half of the Task size. - final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); - final ITaskFragmentOrganizer iOrganizer = - ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); - registerTaskFragmentOrganizer(iOrganizer); - final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .setOrganizer(organizer) - .build(); - final Rect taskBounds = new Rect(); - task.getBounds(taskBounds); - taskFragment.setBounds(0, 0, taskBounds.right / 2, taskBounds.bottom); - spyOn(taskFragment); - - assertTrue(mDc.mChangingContainers.isEmpty()); - assertFalse(mDc.mAppTransition.isTransitionSet()); - - // Schedule app transition when reparent activity to a TaskFragment of different size. - final Rect startBounds = new Rect(activity.getBounds()); - activity.reparent(taskFragment, POSITION_TOP); - - // It should transit at TaskFragment level with snapshot on the activity surface. - verify(taskFragment).initializeChangeTransition(activity.getBounds(), activityLeash); - assertTrue(mDc.mChangingContainers.contains(taskFragment)); - assertTrue(mDc.mAppTransition.containsTransitRequest(TRANSIT_CHANGE)); - } - - @Test - public void testGetNextAppTransitionBackgroundColor() { - assumeFalse(WindowManagerService.sEnableShellTransitions); - - // No override by default. - assertEquals(0, mDc.mAppTransition.getNextAppTransitionBackgroundColor()); - - // Override with a custom color. - mDc.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0); - final int testColor = 123; - mDc.mAppTransition.overridePendingAppTransition("testPackage", 0 /* enterAnim */, - 0 /* exitAnim */, testColor, null /* startedCallback */, null /* endedCallback */, - false /* overrideTaskTransaction */); - - assertEquals(testColor, mDc.mAppTransition.getNextAppTransitionBackgroundColor()); - assertTrue(mDc.mAppTransition.isNextAppTransitionOverrideRequested()); - - // Override with ActivityEmbedding remote animation. Background color should be kept. - mDc.mAppTransition.overridePendingAppTransitionRemote(mock(RemoteAnimationAdapter.class), - false /* sync */, true /* isActivityEmbedding */); - - assertEquals(testColor, mDc.mAppTransition.getNextAppTransitionBackgroundColor()); - assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested()); - - // Background color should not be cleared anymore after #clear(). - mDc.mAppTransition.clear(); - assertEquals(0, mDc.mAppTransition.getNextAppTransitionBackgroundColor()); - assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested()); - } - - @Test - public void testGetNextAppRequestedAnimation() { - assumeFalse(WindowManagerService.sEnableShellTransitions); - final String packageName = "testPackage"; - final int enterAnimResId = 1; - final int exitAnimResId = 2; - final int testColor = 123; - final Animation enterAnim = mock(Animation.class); - final Animation exitAnim = mock(Animation.class); - final TransitionAnimation transitionAnimation = mDc.mAppTransition.mTransitionAnimation; - spyOn(transitionAnimation); - doReturn(enterAnim).when(transitionAnimation) - .loadAppTransitionAnimation(packageName, enterAnimResId); - doReturn(exitAnim).when(transitionAnimation) - .loadAppTransitionAnimation(packageName, exitAnimResId); - - // No override by default. - assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */)); - assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */)); - - // Override with a custom animation. - mDc.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0); - mDc.mAppTransition.overridePendingAppTransition(packageName, enterAnimResId, exitAnimResId, - testColor, null /* startedCallback */, null /* endedCallback */, - false /* overrideTaskTransaction */); - - assertEquals(enterAnim, mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */)); - assertEquals(exitAnim, mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */)); - assertTrue(mDc.mAppTransition.isNextAppTransitionOverrideRequested()); - - // Override with ActivityEmbedding remote animation. Custom animation should be kept. - mDc.mAppTransition.overridePendingAppTransitionRemote(mock(RemoteAnimationAdapter.class), - false /* sync */, true /* isActivityEmbedding */); - - assertEquals(enterAnim, mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */)); - assertEquals(exitAnim, mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */)); - assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested()); - - // Custom animation should not be cleared anymore after #clear(). - mDc.mAppTransition.clear(); - assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */)); - assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */)); - } - - private class TestRemoteAnimationRunner implements IRemoteAnimationRunner { - boolean mCancelled = false; - @Override - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { - } - - @Override - public void onAnimationCancelled() { - mCancelled = true; - } - - @Override - public IBinder asBinder() { - return null; - } - } -} 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 0964ebed9d25..82435b24dad6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1172,11 +1172,12 @@ public class DisplayContentTests extends WindowTestsBase { .setScreenOrientation(getRotatedOrientation(mDisplayContent)).build(); prev.setVisibleRequested(false); final ActivityRecord top = new ActivityBuilder(mAtm).setCreateTask(true) + .setVisible(false) .setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build(); assertNotEquals(WindowConfiguration.ROTATION_UNDEFINED, mDisplayContent.rotationForActivityInDifferentOrientation(top)); - mDisplayContent.requestTransitionAndLegacyPrepare(WindowManager.TRANSIT_OPEN, 0); + requestTransition(top, WindowManager.TRANSIT_OPEN); top.setVisibility(true); mDisplayContent.updateOrientation(); // The top uses "behind", so the orientation is decided by the previous. @@ -1609,8 +1610,7 @@ public class DisplayContentTests extends WindowTestsBase { final ActivityRecord app = mAppWindow.mActivityRecord; app.setVisible(false); app.setVisibleRequested(false); - registerTestTransitionPlayer(); - mDisplayContent.requestTransitionAndLegacyPrepare(WindowManager.TRANSIT_OPEN, 0); + requestTransition(app, WindowManager.TRANSIT_OPEN); app.setVisibility(true); final int newOrientation = getRotatedOrientation(mDisplayContent); app.setRequestedOrientation(newOrientation); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 6c5fe1d8551e..71e34ef220d3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -53,6 +53,7 @@ import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -400,9 +401,9 @@ public class InsetsPolicyTest extends WindowTestsBase { assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars())); assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars())); - mAppWindow.setRequestedVisibleTypes( + final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes( navigationBars() | statusBars(), navigationBars() | statusBars()); - policy.onRequestedVisibleTypesChanged(mAppWindow, null /* statsToken */); + policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */); waitUntilWindowAnimatorIdle(); controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 973c8d0a8464..5525bae89138 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -52,6 +52,7 @@ import android.util.SparseArray; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.WindowInsets.Type.InsetsType; import androidx.test.filters.SmallTest; @@ -201,8 +202,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getOrCreateSourceProvider(ID_IME, ime()) .setWindowContainer(mImeWindow, null, null); getController().onImeControlTargetChanged(base); - base.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(base, null /* statsToken */); + final @InsetsType int changedTypes = base.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(base, changedTypes, null /* statsToken */); if (android.view.inputmethod.Flags.refactorInsetsController()) { // to set the serverVisibility, the IME needs to be drawn and onPostLayout be called. mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN; @@ -509,8 +510,8 @@ public class InsetsStateControllerTest extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(app); mDisplayContent.updateImeInputAndControlTarget(app); - app.setRequestedVisibleTypes(ime(), ime()); - getController().onRequestedVisibleTypesChanged(app, null /* statsToken */); + final @InsetsType int changedTypes = app.setRequestedVisibleTypes(ime(), ime()); + getController().onRequestedVisibleTypesChanged(app, changedTypes, null /* statsToken */); assertTrue(ime.getControllableInsetProvider().getSource().isVisible()); if (android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java new file mode 100644 index 000000000000..db90c28ec7df --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.Display.FLAG_PRESENTATION; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; + +import android.graphics.Rect; +import android.os.UserHandle; +import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; +import android.view.IWindow; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:PresentationControllerTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class PresentationControllerTests extends WindowTestsBase { + + @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) + @Test + public void testPresentationHidesActivitiesBehind() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.flags = FLAG_PRESENTATION; + final DisplayContent dc = createNewDisplay(displayInfo); + final int displayId = dc.getDisplayId(); + doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); + final ActivityRecord activity = createActivityRecord(createTask(dc)); + assertTrue(activity.isVisible()); + + doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); + final int uid = 100000; // uid for non-system user + final Session session = createTestSession(mAtm, 1234 /* pid */, uid); + final int userId = UserHandle.getUserId(uid); + doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); + final WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_PRESENTATION); + + final IWindow clientWindow = new TestIWindow(); + final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, + userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), + new InsetsSourceControl.Array(), new Rect(), new float[1]); + assertTrue(result >= WindowManagerGlobal.ADD_OKAY); + assertFalse(activity.isVisible()); + + final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); + window.removeImmediately(); + assertTrue(activity.isVisible()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java index fc4f54a431d6..e4a1bf603cf0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java @@ -454,31 +454,6 @@ public class RootWindowContainerTests extends WindowTestsBase { } @Test - public void testMovingBottomMostRootTaskActivityToPinnedRootTask() { - final Task fullscreenTask = mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask( - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); - final ActivityRecord firstActivity = new ActivityBuilder(mAtm) - .setTask(fullscreenTask).build(); - final Task task = firstActivity.getTask(); - - final ActivityRecord secondActivity = new ActivityBuilder(mAtm) - .setTask(fullscreenTask).build(); - - fullscreenTask.moveTaskToBack(task); - - // Ensure full screen task has both tasks. - ensureTaskPlacement(fullscreenTask, firstActivity, secondActivity); - assertEquals(task.getTopMostActivity(), secondActivity); - firstActivity.setState(STOPPED, "testMovingBottomMostRootTaskActivityToPinnedRootTask"); - - - // Move first activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(secondActivity, "initialMove"); - - assertTrue(firstActivity.mRequestForceTransition); - } - - @Test public void testMultipleActivitiesTaskEnterPip() { // Enable shell transition because the order of setting windowing mode is different. registerTestTransitionPlayer(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index dba463a436c0..95bca2b17efb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1811,9 +1811,9 @@ public class SizeCompatTests extends WindowTestsBase { } addStatusBar(mActivity.mDisplayContent); - mActivity.setVisible(false); - mActivity.mDisplayContent.prepareAppTransition(WindowManager.TRANSIT_OPEN); - mActivity.mDisplayContent.mOpeningApps.add(mActivity); + mActivity.setVisibleRequested(false); + requestTransition(mActivity, WindowManager.TRANSIT_OPEN); + mActivity.setVisibility(true); final float maxAspect = 1.8f; prepareUnresizable(mActivity, maxAspect, SCREEN_ORIENTATION_LANDSCAPE); diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java index 7dba1422d61d..2544550120d4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java @@ -22,6 +22,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -69,7 +70,6 @@ public class SurfaceAnimatorTest extends WindowTestsBase { private MyAnimatable mAnimatable; private MyAnimatable mAnimatable2; - private DeferFinishAnimatable mDeferFinishAnimatable; @Before public void setUp() throws Exception { @@ -77,14 +77,12 @@ public class SurfaceAnimatorTest extends WindowTestsBase { mAnimatable = new MyAnimatable(mWm, mTransaction); mAnimatable2 = new MyAnimatable(mWm, mTransaction); - mDeferFinishAnimatable = new DeferFinishAnimatable(mWm, mTransaction); } @After public void tearDown() { mAnimatable = null; mAnimatable2 = null; - mDeferFinishAnimatable = null; } @Test @@ -202,41 +200,33 @@ public class SurfaceAnimatorTest extends WindowTestsBase { } @Test - public void testDeferFinish() { - - // Start animation - final OnAnimationFinishedCallback onFinishedCallback = startDeferFinishAnimatable(mSpec); - - // Finish the animation but then make sure we are deferring. - onFinishedCallback.onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, mSpec); - assertAnimating(mDeferFinishAnimatable); - - // Now end defer finishing. - mDeferFinishAnimatable.mEndDeferFinishCallback.run(); - assertNotAnimating(mAnimatable2); - assertTrue(mDeferFinishAnimatable.mFinishedCallbackCalled); - assertEquals(ANIMATION_TYPE_APP_TRANSITION, mDeferFinishAnimatable.mFinishedAnimationType); - verify(mTransaction).remove(eq(mDeferFinishAnimatable.mLeash)); - } - - @Test public void testDeferFinishDoNotFinishNextAnimation() { + final DeferredFinishAdapter deferredFinishAdapter = new DeferredFinishAdapter(); + spyOn(deferredFinishAdapter); // Start the first animation. - final OnAnimationFinishedCallback onFinishedCallback = startDeferFinishAnimatable(mSpec); - onFinishedCallback.onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, mSpec); + mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, deferredFinishAdapter, + true /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION); + assertAnimating(mAnimatable); + final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass( + OnAnimationFinishedCallback.class); + verify(deferredFinishAdapter).startAnimation(any(), any(), + eq(ANIMATION_TYPE_WINDOW_ANIMATION), callbackCaptor.capture()); + final OnAnimationFinishedCallback onFinishedCallback = callbackCaptor.getValue(); + onFinishedCallback.onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, + deferredFinishAdapter); // The callback is the resetAndInvokeFinish in {@link SurfaceAnimator#getFinishedCallback}. - final Runnable firstDeferFinishCallback = mDeferFinishAnimatable.mEndDeferFinishCallback; + final Runnable firstDeferFinishCallback = deferredFinishAdapter.mEndDeferFinishCallback; // Start the second animation. - mDeferFinishAnimatable.mSurfaceAnimator.cancelAnimation(); - startDeferFinishAnimatable(mSpec2); - mDeferFinishAnimatable.mFinishedCallbackCalled = false; + mAnimatable.mSurfaceAnimator.cancelAnimation(); + mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec2, + true /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION); + mAnimatable.mFinishedCallbackCalled = false; - // Simulate the first deferred callback is executed from - // {@link AnimatingActivityRegistry#endDeferringFinished}. + // Simulate the first deferred callback is executed. firstDeferFinishCallback.run(); // The second animation should not be finished. - assertFalse(mDeferFinishAnimatable.mFinishedCallbackCalled); + assertFalse(mAnimatable.mFinishedCallbackCalled); } @Test @@ -260,17 +250,6 @@ public class SurfaceAnimatorTest extends WindowTestsBase { verify(mTransaction).remove(eq(deferredFinishAdapter.mAnimationLeash)); } - private OnAnimationFinishedCallback startDeferFinishAnimatable(AnimationAdapter anim) { - mDeferFinishAnimatable.mSurfaceAnimator.startAnimation(mTransaction, anim, - true /* hidden */, ANIMATION_TYPE_APP_TRANSITION); - final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass( - OnAnimationFinishedCallback.class); - assertAnimating(mDeferFinishAnimatable); - verify(anim).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), - callbackCaptor.capture()); - return callbackCaptor.getValue(); - } - private void assertAnimating(MyAnimatable animatable) { assertTrue(animatable.mSurfaceAnimator.isAnimating()); assertNotNull(animatable.mSurfaceAnimator.getAnimation()); @@ -370,21 +349,6 @@ public class SurfaceAnimatorTest extends WindowTestsBase { }; } - private static class DeferFinishAnimatable extends MyAnimatable { - - Runnable mEndDeferFinishCallback; - - DeferFinishAnimatable(WindowManagerService wm, Transaction transaction) { - super(wm, transaction); - } - - @Override - public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { - mEndDeferFinishCallback = endDeferFinishCallback; - return true; - } - } - private static class DeferredFinishAdapter implements AnimationAdapter { private Runnable mEndDeferFinishCallback; diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 001446550304..edffab801499 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -32,8 +32,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; -import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; @@ -85,12 +83,8 @@ import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArraySet; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; import android.view.InsetsFrameProvider; import android.view.InsetsSource; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowInsets; @@ -1055,25 +1049,6 @@ public class WindowContainerTests extends WindowTestsBase { } @Test - public void testTaskCanApplyAnimation() { - final Task rootTask = createTask(mDisplayContent); - final Task task = createTaskInRootTask(rootTask, 0 /* userId */); - final ActivityRecord activity2 = createActivityRecord(mDisplayContent, task); - final ActivityRecord activity1 = createActivityRecord(mDisplayContent, task); - verifyWindowContainerApplyAnimation(task, activity1, activity2); - } - - @Test - public void testRootTaskCanApplyAnimation() { - final Task rootTask = createTask(mDisplayContent); - final ActivityRecord activity2 = createActivityRecord(mDisplayContent, - createTaskInRootTask(rootTask, 0 /* userId */)); - final ActivityRecord activity1 = createActivityRecord(mDisplayContent, - createTaskInRootTask(rootTask, 0 /* userId */)); - verifyWindowContainerApplyAnimation(rootTask, activity1, activity2); - } - - @Test public void testGetDisplayArea() { // WindowContainer final WindowContainer windowContainer = new WindowContainer(mWm); @@ -1103,59 +1078,6 @@ public class WindowContainerTests extends WindowTestsBase { assertEquals(displayArea, displayArea.getDisplayArea()); } - private void verifyWindowContainerApplyAnimation(WindowContainer wc, ActivityRecord act, - ActivityRecord act2) { - // Initial remote animation for app transition. - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - new IRemoteAnimationRunner.Stub() { - @Override - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - @Override - public void onAnimationCancelled() { - } - }, 0, 0, false); - adapter.setCallingPidUid(123, 456); - wc.getDisplayContent().prepareAppTransition(TRANSIT_OPEN); - wc.getDisplayContent().mAppTransition.overridePendingAppTransitionRemote(adapter); - spyOn(wc); - doReturn(true).when(wc).okToAnimate(); - - // Make sure animating state is as expected after applied animation. - - // Animation target is promoted from act to wc. act2 is a descendant of wc, but not a source - // of the animation. - ArrayList<WindowContainer<WindowState>> sources = new ArrayList<>(); - sources.add(act); - assertTrue(wc.applyAnimation(null, TRANSIT_OLD_TASK_OPEN, true, false, sources)); - - assertEquals(act, wc.getTopMostActivity()); - assertTrue(wc.isAnimating()); - assertTrue(wc.isAnimating(0, ANIMATION_TYPE_APP_TRANSITION)); - assertTrue(wc.getAnimationSources().contains(act)); - assertFalse(wc.getAnimationSources().contains(act2)); - assertTrue(act.isAnimating(PARENTS)); - assertTrue(act.isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION)); - assertEquals(wc, act.getAnimatingContainer(PARENTS, ANIMATION_TYPE_APP_TRANSITION)); - - // Make sure animation finish callback will be received and reset animating state after - // animation finish. - wc.getDisplayContent().mAppTransition.goodToGo(TRANSIT_OLD_TASK_OPEN, act); - verify(wc).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), any()); - assertFalse(wc.isAnimating()); - assertFalse(act.isAnimating(PARENTS)); - } - @Test public void testRegisterWindowContainerListener() { final WindowContainer container = new WindowContainer(mWm); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 1323d8a59cef..71e84c0f1821 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -26,7 +26,6 @@ import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCRE import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_OWN_FOCUS; -import static android.view.Display.FLAG_PRESENTATION; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; @@ -55,7 +54,6 @@ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS; import static com.google.common.truth.Truth.assertThat; @@ -102,7 +100,6 @@ import android.provider.Settings; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; -import android.view.DisplayInfo; import android.view.IWindow; import android.view.InputChannel; import android.view.InputDevice; @@ -1409,38 +1406,6 @@ public class WindowManagerServiceTests extends WindowTestsBase { assertEquals(activityWindowInfo2, activityWindowInfo3); } - @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS) - @Test - public void testPresentationHidesActivitiesBehind() { - DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.copyFrom(mDisplayInfo); - displayInfo.flags = FLAG_PRESENTATION; - DisplayContent dc = createNewDisplay(displayInfo); - int displayId = dc.getDisplayId(); - doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId); - ActivityRecord activity = createActivityRecord(createTask(dc)); - assertTrue(activity.isVisible()); - - doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); - int uid = 100000; // uid for non-system user - Session session = createTestSession(mAtm, 1234 /* pid */, uid); - int userId = UserHandle.getUserId(uid); - doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId)); - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - LayoutParams.TYPE_PRESENTATION); - - final IWindow clientWindow = new TestIWindow(); - int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, - userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(), - new InsetsSourceControl.Array(), new Rect(), new float[1]); - assertTrue(result >= WindowManagerGlobal.ADD_OKAY); - assertFalse(activity.isVisible()); - - final WindowState window = mWm.windowForClientLocked(session, clientWindow, false); - window.removeImmediately(); - assertTrue(activity.isVisible()); - } - @Test public void testAddOverlayWindowToUnassignedDisplay_notAllowed_ForVisibleBackgroundUsers() { doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 1281be5132d3..7030d986494f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -26,6 +26,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; @@ -33,6 +34,8 @@ import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; +import static android.content.res.Configuration.UI_MODE_NIGHT_NO; +import static android.content.res.Configuration.UI_MODE_NIGHT_YES; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST; @@ -1983,6 +1986,30 @@ public class WindowOrganizerTests extends WindowTestsBase { testSetAlwaysOnTop(displayArea); } + @Test + public void testConfigurationsAreEqualForOrganizer() { + Configuration config1 = new Configuration(); + config1.smallestScreenWidthDp = 300; + config1.uiMode = UI_MODE_NIGHT_YES; + + Configuration config2 = new Configuration(config1); + config2.uiMode = UI_MODE_NIGHT_NO; + + Configuration config3 = new Configuration(config1); + config3.smallestScreenWidthDp = 500; + + // Should be equal for non-controllable configuration changes. + assertTrue(WindowOrganizerController.configurationsAreEqualForOrganizer(config1, config2)); + + // Should be unequal for non-controllable configuration changes if the organizer is + // interested in that change. + assertFalse(WindowOrganizerController.configurationsAreEqualForOrganizer( + config1, config2, CONFIG_UI_MODE)); + + // Should be unequal for controllable configuration changes. + assertFalse(WindowOrganizerController.configurationsAreEqualForOrganizer(config1, config3)); + } + private void testSetAlwaysOnTop(WindowContainer wc) { final WindowContainerTransaction t = new WindowContainerTransaction(); t.setAlwaysOnTop(wc.mRemoteToken.toWindowContainerToken(), true); 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 b16f5283d532..7f9e591ca5e3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -961,6 +961,15 @@ public class WindowTestsBase extends SystemServiceTestsBase { return testPlayer; } + void requestTransition(WindowContainer<?> wc, int transit) { + final TransitionController controller = mRootWindowContainer.mTransitionController; + if (controller.getTransitionPlayer() == null) { + registerTestTransitionPlayer(); + } + controller.requestTransitionIfNeeded(transit, 0 /* flags */, null /* trigger */, + wc.mDisplayContent); + } + /** Overrides the behavior of config_reverseDefaultRotation for the given display. */ void setReverseDefaultRotation(DisplayContent dc, boolean reverse) { final DisplayRotation displayRotation = dc.getDisplayRotation(); @@ -1417,7 +1426,9 @@ public class WindowTestsBase extends SystemServiceTestsBase { activity.setProcess(wpc); // Resume top activities to make sure all other signals in the system are connected. - mService.mRootWindowContainer.resumeFocusedTasksTopActivities(); + if (mVisible) { + mService.mRootWindowContainer.resumeFocusedTasksTopActivities(); + } return activity; } } diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml index 37321ad80b0f..758852bb1074 100644 --- a/tests/AttestationVerificationTest/AndroidManifest.xml +++ b/tests/AttestationVerificationTest/AndroidManifest.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.security.attestationverification"> - <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" /> <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> <application> diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json new file mode 100644 index 000000000000..2a3ba5ebde7d --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json @@ -0,0 +1,12 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json new file mode 100644 index 000000000000..e22a834a92bf --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json @@ -0,0 +1,16 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "353017e73dc205a73a9c3de142230370" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java new file mode 100644 index 000000000000..c38517ace5e6 --- /dev/null +++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.security; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.os.SystemClock; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class CertificateRevocationStatusManagerTest { + + private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem"; + private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem"; + private static final String TEST_REVOCATION_LIST_FILE_NAME = "test_revocation_list.json"; + private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_no_test_certs.json"; + private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_with_test_certs.json"; + private static final String TEST_REVOCATION_STATUS_FILE_NAME = "test_revocation_status.txt"; + private static final String FILE_URL_PREFIX = "file://"; + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + private CertificateFactory mFactory; + private List<X509Certificate> mCertificates1; + private List<X509Certificate> mCertificates2; + private File mRevocationListFile; + private String mRevocationListUrl; + private String mNonExistentRevocationListUrl; + private File mRevocationStatusFile; + private CertificateRevocationStatusManager mCertificateRevocationStatusManager; + + @Before + public void setUp() throws Exception { + mFactory = CertificateFactory.getInstance("X.509"); + mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1); + mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2); + mRevocationListFile = new File(mContext.getFilesDir(), TEST_REVOCATION_LIST_FILE_NAME); + mRevocationListUrl = FILE_URL_PREFIX + mRevocationListFile.getAbsolutePath(); + File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist"); + mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath(); + mRevocationStatusFile = new File(mContext.getFilesDir(), TEST_REVOCATION_STATUS_FILE_NAME); + } + + @After + public void tearDown() throws Exception { + mRevocationListFile.delete(); + mRevocationStatusFile.delete(); + } + + @Test + public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void + checkRevocationStatus_cannotReachRemoteRevocationList_noStoredStatus_throwsException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_savesRevocationStatus() throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + + assertThat(mRevocationStatusFile.length()).isGreaterThan(0); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_certsSaved_noException() + throws Exception { + // call checkRevocationStatus once to save the revocation status + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // call checkRevocationStatus again with mNonExistentRevocationListUrl + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_someCertsNotSaved_exception() + throws Exception { + // call checkRevocationStatus once to save the revocation status for mCertificates2 + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates2); + // call checkRevocationStatus again with mNonExistentRevocationListUrl, this time for + // mCertificates1 + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_someCertsStatusTooOld_exception() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expiredStatusDate = + now.minusDays(CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK + 1); + Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>(); + lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(0)), expiredStatusDate); + for (int i = 1; i < mCertificates1.size(); i++) { + lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(i)), now); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_allCertResultsFresh_noException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + LocalDateTime bearlyNotExpiredStatusDate = + LocalDateTime.now() + .minusDays( + CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK - 1); + Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>(); + for (X509Certificate certificate : mCertificates1) { + lastRevocationCheckData.put(getSerialNumber(certificate), bearlyNotExpiredStatusDate); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void updateLastRevocationCheckData_correctlySavesStatus() throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false); + Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); + for (X509Certificate certificate : mCertificates1) { + areCertificatesRevoked.put(getSerialNumber(certificate), false); + } + + mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked); + + // no exception + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // revoke one certificate and try again + areCertificatesRevoked.put(getSerialNumber(mCertificates1.getLast()), true); + mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked); + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void updateLastRevocationCheckDataForAllPreviouslySeenCertificates_updatesCorrectly() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + // populate the revocation status file + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // Sleep for 2 second so that the current time changes + SystemClock.sleep(2000); + LocalDateTime timestampBeforeUpdate = LocalDateTime.now(); + JSONObject revocationList = mCertificateRevocationStatusManager.fetchRemoteRevocationList(); + List<String> otherCertificatesToCheck = new ArrayList<>(); + String serialNumber1 = "1234567"; // not revoked + String serialNumber2 = "8350192447815228107"; // revoked + String serialNumber3 = "987654"; // not revoked + otherCertificatesToCheck.add(serialNumber1); + otherCertificatesToCheck.add(serialNumber2); + otherCertificatesToCheck.add(serialNumber3); + + mCertificateRevocationStatusManager + .updateLastRevocationCheckDataForAllPreviouslySeenCertificates( + revocationList, otherCertificatesToCheck); + + Map<String, LocalDateTime> lastRevocationCheckData = + mCertificateRevocationStatusManager.getLastRevocationCheckData(); + assertThat(lastRevocationCheckData.get(serialNumber1)).isAtLeast(timestampBeforeUpdate); + assertThat(lastRevocationCheckData).doesNotContainKey(serialNumber2); // revoked + assertThat(lastRevocationCheckData.get(serialNumber3)).isAtLeast(timestampBeforeUpdate); + // validate that the existing certificates on the file got updated too + for (X509Certificate certificate : mCertificates1) { + assertThat(lastRevocationCheckData.get(getSerialNumber(certificate))) + .isAtLeast(timestampBeforeUpdate); + } + } + + private List<X509Certificate> getCertificateChain(String fileName) throws Exception { + Collection<? extends Certificate> certificates = + mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName)); + ArrayList<X509Certificate> x509Certs = new ArrayList<>(); + for (Certificate cert : certificates) { + x509Certs.add((X509Certificate) cert); + } + return x509Certs; + } + + private void copyFromAssetToFile(String assetFileName, File targetFile) throws Exception { + byte[] data; + try (InputStream in = mContext.getResources().getAssets().open(assetFileName)) { + data = in.readAllBytes(); + } + try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) { + fileOutputStream.write(data); + } + } + + private String getSerialNumber(X509Certificate certificate) { + return certificate.getSerialNumber().toString(16); + } +} diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index 12670cda74b2..ac704e5e7c39 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -52,10 +52,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 481a8bb66fee..1b2007deae27 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -50,10 +50,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> |