diff options
50 files changed, 1306 insertions, 743 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 90c37d1999e1..4ea0c32d3b36 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -308,7 +308,7 @@ import java.util.function.Consumer; * <li>The <b>foreground lifetime</b> of an activity happens between a call to * {@link android.app.Activity#onResume} until a corresponding call to * {@link android.app.Activity#onPause}. During this time the activity is - * in visible, active and interacting with the user. An activity + * visible, active and interacting with the user. An activity * can frequently go between the resumed and paused states -- for example when * the device goes to sleep, when an activity result is delivered, when a new * intent is delivered -- so the code in these methods should be fairly diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 336ef7ac78db..41822e77f953 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -1693,8 +1693,10 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { ((i != idx) || notifyCurrentIndex)) { TotalCaptureResult result = previewMap.valueAt(i).second; Long timestamp = result.get(CaptureResult.SENSOR_TIMESTAMP); - mCaptureResultHandler.onCaptureCompleted(timestamp, - initializeFilteredResults(result)); + if (mCaptureResultHandler != null) { + mCaptureResultHandler.onCaptureCompleted(timestamp, + initializeFilteredResults(result)); + } Log.w(TAG, "Preview frame drop with timestamp: " + previewMap.keyAt(i)); final long ident = Binder.clearCallingIdentity(); diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index 98ef4e7ae6fa..90384b520315 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -64,6 +64,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -630,7 +631,11 @@ public final class MainContentCaptureSession extends ContentCaptureSession { mComponentName = null; mEvents = null; if (mDirectServiceInterface != null) { - mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0); + try { + mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0); + } catch (NoSuchElementException e) { + Log.w(TAG, "IContentCaptureDirectManager does not exist"); + } } mDirectServiceInterface = null; mHandler.removeMessages(MSG_FLUSH); diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java index f4f438b1f601..8af2450f29ef 100644 --- a/core/java/com/android/internal/util/ScreenshotHelper.java +++ b/core/java/com/android/internal/util/ScreenshotHelper.java @@ -1,7 +1,6 @@ package com.android.internal.util; import static android.content.Intent.ACTION_USER_SWITCHED; -import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER; import android.annotation.NonNull; import android.annotation.Nullable; @@ -29,6 +28,10 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.view.WindowManager; +import android.view.WindowManager.ScreenshotSource; +import android.view.WindowManager.ScreenshotType; + +import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; import java.util.function.Consumer; @@ -42,24 +45,28 @@ public class ScreenshotHelper { * Describes a screenshot request (to make it easier to pass data through to the handler). */ public static class ScreenshotRequest implements Parcelable { - private int mSource; - private boolean mHasStatusBar; - private boolean mHasNavBar; - private Bundle mBitmapBundle; - private Rect mBoundsInScreen; - private Insets mInsets; - private int mTaskId; - private int mUserId; - private ComponentName mTopComponent; - - ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) { + private final int mSource; + private final Bundle mBitmapBundle; + private final Rect mBoundsInScreen; + private final Insets mInsets; + private final int mTaskId; + private final int mUserId; + private final ComponentName mTopComponent; + + @VisibleForTesting + public ScreenshotRequest(int source) { mSource = source; - mHasStatusBar = hasStatus; - mHasNavBar = hasNav; + mBitmapBundle = null; + mBoundsInScreen = null; + mInsets = null; + mTaskId = -1; + mUserId = -1; + mTopComponent = null; } - ScreenshotRequest(int source, Bundle bitmapBundle, Rect boundsInScreen, Insets insets, - int taskId, int userId, ComponentName topComponent) { + @VisibleForTesting + public ScreenshotRequest(int source, Bundle bitmapBundle, Rect boundsInScreen, + Insets insets, int taskId, int userId, ComponentName topComponent) { mSource = source; mBitmapBundle = bitmapBundle; mBoundsInScreen = boundsInScreen; @@ -71,16 +78,21 @@ public class ScreenshotHelper { ScreenshotRequest(Parcel in) { mSource = in.readInt(); - mHasStatusBar = in.readBoolean(); - mHasNavBar = in.readBoolean(); - if (in.readInt() == 1) { mBitmapBundle = in.readBundle(getClass().getClassLoader()); - mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), android.graphics.Rect.class); - mInsets = in.readParcelable(Insets.class.getClassLoader(), android.graphics.Insets.class); + mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), Rect.class); + mInsets = in.readParcelable(Insets.class.getClassLoader(), Insets.class); mTaskId = in.readInt(); mUserId = in.readInt(); - mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(), android.content.ComponentName.class); + mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(), + ComponentName.class); + } else { + mBitmapBundle = null; + mBoundsInScreen = null; + mInsets = null; + mTaskId = -1; + mUserId = -1; + mTopComponent = null; } } @@ -88,14 +100,6 @@ public class ScreenshotHelper { return mSource; } - public boolean getHasStatusBar() { - return mHasStatusBar; - } - - public boolean getHasNavBar() { - return mHasNavBar; - } - public Bundle getBitmapBundle() { return mBitmapBundle; } @@ -112,7 +116,6 @@ public class ScreenshotHelper { return mTaskId; } - public int getUserId() { return mUserId; } @@ -129,8 +132,6 @@ public class ScreenshotHelper { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mSource); - dest.writeBoolean(mHasStatusBar); - dest.writeBoolean(mHasNavBar); if (mBitmapBundle == null) { dest.writeInt(0); } else { @@ -144,7 +145,8 @@ public class ScreenshotHelper { } } - public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR = + @NonNull + public static final Parcelable.Creator<ScreenshotRequest> CREATOR = new Parcelable.Creator<ScreenshotRequest>() { @Override @@ -254,113 +256,71 @@ public class ScreenshotHelper { /** * Request a screenshot be taken. - * + * <p> * Added to support reducing unit test duration; the method variant without a timeout argument * is recommended for general use. * - * @param screenshotType The type of screenshot, for example either - * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} - * or - * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} - * @param hasStatus {@code true} if the status bar is currently showing. {@code false} - * if not. - * @param hasNav {@code true} if the navigation bar is currently showing. {@code - * false} if not. - * @param source The source of the screenshot request. One of - * {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD, - * SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER} - * @param handler A handler used in case the screenshot times out - * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the - * screenshot was taken. + * @param screenshotType The type of screenshot, defined by {@link ScreenshotType} + * @param source The source of the screenshot request, defined by {@link ScreenshotSource} + * @param handler used to process messages received from the screenshot service + * @param completionConsumer receives the URI of the captured screenshot, once saved or + * null if no screenshot was saved */ - public void takeScreenshot(final int screenshotType, final boolean hasStatus, - final boolean hasNav, int source, @NonNull Handler handler, - @Nullable Consumer<Uri> completionConsumer) { - ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav); - takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest, - completionConsumer); - } - - /** - * Request a screenshot be taken, with provided reason. - * - * @param screenshotType The type of screenshot, for example either - * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} - * or - * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} - * @param hasStatus {@code true} if the status bar is currently showing. {@code false} - * if - * not. - * @param hasNav {@code true} if the navigation bar is currently showing. {@code - * false} - * if not. - * @param handler A handler used in case the screenshot times out - * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the - * screenshot was taken. - */ - public void takeScreenshot(final int screenshotType, final boolean hasStatus, - final boolean hasNav, @NonNull Handler handler, - @Nullable Consumer<Uri> completionConsumer) { - takeScreenshot(screenshotType, hasStatus, hasNav, SCREENSHOT_TIMEOUT_MS, handler, + public void takeScreenshot(@ScreenshotType int screenshotType, @ScreenshotSource int source, + @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) { + ScreenshotRequest screenshotRequest = new ScreenshotRequest(source); + takeScreenshot(screenshotType, handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS, completionConsumer); } /** - * Request a screenshot be taken with a specific timeout. - * + * Request a screenshot be taken. + * <p> * Added to support reducing unit test duration; the method variant without a timeout argument * is recommended for general use. * - * @param screenshotType The type of screenshot, for example either - * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} - * or - * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} - * @param hasStatus {@code true} if the status bar is currently showing. {@code false} - * if - * not. - * @param hasNav {@code true} if the navigation bar is currently showing. {@code - * false} - * if not. - * @param timeoutMs If the screenshot hasn't been completed within this time period, - * the screenshot attempt will be cancelled and `completionConsumer` - * will be run. - * @param handler A handler used in case the screenshot times out - * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the - * screenshot was taken. + * @param screenshotType The type of screenshot, defined by {@link ScreenshotType} + * @param source The source of the screenshot request, defined by {@link ScreenshotSource} + * @param handler used to process messages received from the screenshot service + * @param timeoutMs time limit for processing, intended only for testing + * @param completionConsumer receives the URI of the captured screenshot, once saved or + * null if no screenshot was saved */ - public void takeScreenshot(final int screenshotType, final boolean hasStatus, - final boolean hasNav, long timeoutMs, @NonNull Handler handler, - @Nullable Consumer<Uri> completionConsumer) { - ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus, - hasNav); - takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer); + @VisibleForTesting + public void takeScreenshot(@ScreenshotType int screenshotType, @ScreenshotSource int source, + @NonNull Handler handler, long timeoutMs, @Nullable Consumer<Uri> completionConsumer) { + ScreenshotRequest screenshotRequest = new ScreenshotRequest(source); + takeScreenshot(screenshotType, handler, screenshotRequest, timeoutMs, completionConsumer); } /** * Request that provided image be handled as if it was a screenshot. * - * @param screenshotBundle Bundle containing the buffer and color space of the screenshot. - * @param boundsInScreen The bounds in screen coordinates that the bitmap orginated from. - * @param insets The insets that the image was shown with, inside the screenbounds. - * @param taskId The taskId of the task that the screen shot was taken of. - * @param userId The userId of user running the task provided in taskId. - * @param topComponent The component name of the top component running in the task. - * @param handler A handler used in case the screenshot times out - * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the - * screenshot was taken. + * @param screenshotBundle Bundle containing the buffer and color space of the screenshot. + * @param boundsInScreen The bounds in screen coordinates that the bitmap originated from. + * @param insets The insets that the image was shown with, inside the screen bounds. + * @param taskId The taskId of the task that the screen shot was taken of. + * @param userId The userId of user running the task provided in taskId. + * @param topComponent The component name of the top component running in the task. + * @param source The source of the screenshot request, defined by {@link ScreenshotSource} + * @param handler A handler used in case the screenshot times out + * @param completionConsumer receives the URI of the captured screenshot, once saved or + * null if no screenshot was saved */ public void provideScreenshot(@NonNull Bundle screenshotBundle, @NonNull Rect boundsInScreen, - @NonNull Insets insets, int taskId, int userId, ComponentName topComponent, int source, - @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) { - ScreenshotRequest screenshotRequest = - new ScreenshotRequest(source, screenshotBundle, boundsInScreen, insets, taskId, - userId, topComponent); - takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS, - handler, screenshotRequest, completionConsumer); + @NonNull Insets insets, int taskId, int userId, ComponentName topComponent, + @ScreenshotSource int source, @NonNull Handler handler, + @Nullable Consumer<Uri> completionConsumer) { + ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, screenshotBundle, + boundsInScreen, insets, taskId, userId, topComponent); + takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, handler, screenshotRequest, + SCREENSHOT_TIMEOUT_MS, + completionConsumer); } - private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler, - ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) { + private void takeScreenshot(@ScreenshotType int screenshotType, @NonNull Handler handler, + ScreenshotRequest screenshotRequest, long timeoutMs, + @Nullable Consumer<Uri> completionConsumer) { synchronized (mScreenshotLock) { final Runnable mScreenshotTimeout = () -> { diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java index 4b8173732b4d..fd4fb133ef12 100644 --- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java +++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java @@ -80,13 +80,14 @@ public final class ScreenshotHelperTest { @Test public void testFullscreenScreenshot() { - mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, false, false, mHandler, null); + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, + WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null); } @Test public void testSelectedRegionScreenshot() { - mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION, false, false, mHandler, - null); + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION, + WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null); } @Test @@ -101,8 +102,10 @@ public final class ScreenshotHelperTest { long timeoutMs = 10; CountDownLatch lock = new CountDownLatch(1); - mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, false, false, timeoutMs, + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, + WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, + timeoutMs, uri -> { assertNull(uri); lock.countDown(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d7f1292cb717..1c2f0d8ca7b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -70,12 +70,10 @@ import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; -import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseSetArray; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -109,8 +107,10 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -177,8 +177,8 @@ public class BubbleController implements ConfigurationChangeListener { private int mCurrentUserId; // Current profiles of the user (e.g. user with a workprofile) private SparseArray<UserInfo> mCurrentProfiles; - // Saves notification keys of active bubbles when users are switched. - private final SparseSetArray<String> mSavedBubbleKeysPerUser; + // Saves data about active bubbles when users are switched. + private final SparseArray<UserBubbleData> mSavedUserBubbleData; // Used when ranking updates occur and we check if things should bubble / unbubble private NotificationListenerService.Ranking mTmpRanking; @@ -271,7 +271,7 @@ public class BubbleController implements ConfigurationChangeListener { mCurrentUserId = ActivityManager.getCurrentUser(); mBubblePositioner = positioner; mBubbleData = data; - mSavedBubbleKeysPerUser = new SparseSetArray<>(); + mSavedUserBubbleData = new SparseArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context); mDisplayController = displayController; @@ -420,6 +420,13 @@ public class BubbleController implements ConfigurationChangeListener { List<UserInfo> users = mUserManager.getAliveUsers(); mDataRepository.sanitizeBubbles(users); + // Init profiles + SparseArray<UserInfo> userProfiles = new SparseArray<>(); + for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) { + userProfiles.put(user.id, user); + } + mCurrentProfiles = userProfiles; + mShellController.addConfigurationChangeListener(this); } @@ -774,11 +781,13 @@ public class BubbleController implements ConfigurationChangeListener { */ private void saveBubbles(@UserIdInt int userId) { // First clear any existing keys that might be stored. - mSavedBubbleKeysPerUser.remove(userId); + mSavedUserBubbleData.remove(userId); + UserBubbleData userBubbleData = new UserBubbleData(); // Add in all active bubbles for the current user. for (Bubble bubble : mBubbleData.getBubbles()) { - mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); + userBubbleData.add(bubble.getKey(), bubble.showInShade()); } + mSavedUserBubbleData.put(userId, userBubbleData); } /** @@ -787,22 +796,23 @@ public class BubbleController implements ConfigurationChangeListener { * @param userId the id of the user */ private void restoreBubbles(@UserIdInt int userId) { - ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); - if (savedBubbleKeys == null) { + UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId); + if (savedBubbleData == null) { // There were no bubbles saved for this used. return; } - mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> { + mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> { mMainExecutor.execute(() -> { for (BubbleEntry e : entries) { if (canLaunchInTaskView(mContext, e)) { - updateBubble(e, true /* suppressFlyout */, false /* showInShade */); + boolean showInShade = savedBubbleData.isShownInShade(e.getKey()); + updateBubble(e, true /* suppressFlyout */, showInShade); } } }); }); // Finally, remove the entries for this user now that bubbles are restored. - mSavedBubbleKeysPerUser.remove(userId); + mSavedUserBubbleData.remove(userId); } @Override @@ -993,7 +1003,19 @@ public class BubbleController implements ConfigurationChangeListener { */ @VisibleForTesting public void updateBubble(BubbleEntry notif) { - updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + int bubbleUserId = notif.getStatusBarNotification().getUserId(); + if (isCurrentProfile(bubbleUserId)) { + updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + } else { + // Skip update, but store it in user bubbles so it gets restored after user switch + mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(), + true /* shownInShade */); + if (DEBUG_BUBBLE_CONTROLLER) { + Log.d(TAG, + "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId + + " current userId=" + mCurrentUserId); + } + } } /** @@ -1842,4 +1864,33 @@ public class BubbleController implements ConfigurationChangeListener { } } } + + /** + * Bubble data that is stored per user. + * Used to store and restore active bubbles during user switching. + */ + private static class UserBubbleData { + private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>(); + + /** + * Add bubble key and whether it should be shown in notification shade + */ + void add(String key, boolean shownInShade) { + mKeyToShownInShadeMap.put(key, shownInShade); + } + + /** + * Get all bubble keys stored for this user + */ + Set<String> getKeys() { + return mKeyToShownInShadeMap.keySet(); + } + + /** + * Check if this bubble with the given key should be shown in the notification shade + */ + boolean isShownInShade(String key) { + return mKeyToShownInShadeMap.get(key); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index f8ccf2364b4c..cf792cda91b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -23,12 +23,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.pm.UserInfo; -import android.content.res.Configuration; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; -import android.util.ArraySet; import android.util.Pair; import android.util.SparseArray; @@ -42,6 +40,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -284,7 +283,7 @@ public interface Bubbles { void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback); - void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys, + void getShouldRestoredEntries(Set<String> savedBubbleKeys, Consumer<List<BubbleEntry>> callback); void setNotificationInterruption(String key); 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 3c7db33bccf2..66f5bb663fbe 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 @@ -68,7 +68,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.Context; @@ -522,14 +521,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, finishedCallback.onAnimationFinished(); } }; + Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); try { - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - adapter.getCallingApplication()); - } catch (SecurityException e) { - Slog.e(TAG, "Unable to boost animation thread. This should only happen" - + " during unit tests"); - } adapter.getRunner().onAnimationStart(transit, apps, wallpapers, augmentedNonApps, wrapCallback); } catch (RemoteException e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index ebaece2189aa..4e1fa290270d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -18,11 +18,9 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityTaskManager; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; -import android.util.Slog; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; @@ -87,17 +85,11 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { }); } }; + Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread()); try { if (mRemote.asBinder() != null) { mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - mRemote.getAppThread()); - } catch (SecurityException e) { - Slog.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" - + " during unit tests"); - } mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); // assume that remote will apply the start transaction. startTransaction.clear(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index b15c48cb5889..cedb340816ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -18,7 +18,6 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityTaskManager; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; @@ -129,15 +128,9 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { }); } }; + Transitions.setRunningRemoteTransitionDelegate(remote.getAppThread()); try { handleDeath(remote.asBinder(), finishCallback); - try { - ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( - remote.getAppThread()); - } catch (SecurityException e) { - Log.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" - + " during unit tests"); - } remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); // assume that remote will apply the start transaction. startTransaction.clear(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 8e36154d10e7..881b7a1699f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -31,6 +31,8 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityTaskManager; +import android.app.IApplicationThread; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -234,6 +236,19 @@ public class Transitions implements RemoteCallable<Transitions> { mRemoteTransitionHandler.removeFiltered(remoteTransition); } + /** Boosts the process priority of remote animation player. */ + public static void setRunningRemoteTransitionDelegate(IApplicationThread appThread) { + if (appThread == null) return; + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(appThread); + } catch (SecurityException e) { + Log.e(TAG, "Unable to boost animation process. This should only happen" + + " during unit tests"); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + /** * Runs the given {@code runnable} when the last active transition has finished, or immediately * if there are currently no active transitions. diff --git a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml index 5ce08b7747a4..27425589c822 100644 --- a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml +++ b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml @@ -16,7 +16,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.settingslib.activityembedding"> + package="com.android.settingslib.widget"> <uses-sdk android:minSdkVersion="21" /> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml index 1c47f5ff862a..244b367423a4 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml @@ -16,7 +16,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.settingslib.collapsingtoolbar"> + package="com.android.settingslib.widget"> <uses-sdk android:minSdkVersion="29" /> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java index 8ebbac39d1d0..3582897014ff 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java @@ -20,6 +20,7 @@ import androidx.fragment.app.FragmentActivity; import androidx.preference.PreferenceFragmentCompat; import com.android.settingslib.utils.BuildCompatUtils; +import com.android.settingslib.widget.R; import com.google.android.material.appbar.AppBarLayout; diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java index 77d65834da37..8c8b47875bec 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import com.android.settingslib.utils.BuildCompatUtils; +import com.android.settingslib.widget.R; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java index 31e8cc709749..ec091bf4de67 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java @@ -30,6 +30,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.android.settingslib.widget.R; + import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java index 1ead2f337314..a8c7a3f936ee 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java @@ -36,7 +36,7 @@ import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.coordinatorlayout.widget.CoordinatorLayout; -import com.android.settingslib.collapsingtoolbar.R; +import com.android.settingslib.widget.R; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; diff --git a/packages/SettingsLib/SettingsTransition/AndroidManifest.xml b/packages/SettingsLib/SettingsTransition/AndroidManifest.xml index b6aff53e0290..244b367423a4 100644 --- a/packages/SettingsLib/SettingsTransition/AndroidManifest.xml +++ b/packages/SettingsLib/SettingsTransition/AndroidManifest.xml @@ -16,7 +16,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.settingslib.transition"> + package="com.android.settingslib.widget"> <uses-sdk android:minSdkVersion="29" /> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index ffd6b522e394..1344144ee6fc 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -225,6 +225,7 @@ android_library { "androidx.exifinterface_exifinterface", "kotlinx-coroutines-android", "kotlinx-coroutines-core", + "kotlinx_coroutines_test", "iconloader_base", "SystemUI-tags", "SystemUI-proto", diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 448b99b6e5d0..a1288b531955 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -500,7 +500,7 @@ public class SystemActions extends CoreStartable { private void handleTakeScreenshot() { ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); - screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true, + screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null); } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index ab30db297fce..ca65d12e87e6 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -947,7 +947,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mHandler.postDelayed(new Runnable() { @Override public void run() { - mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true, + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU); mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index e913d2b86306..dbf218a115cb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -627,13 +627,13 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, case TelephonyManager.SIM_STATE_PUK_REQUIRED: synchronized (KeyguardViewMediator.this) { mSimWasLocked.append(slotId, true); + mPendingPinLock = true; if (!mShowing) { if (DEBUG_SIM_STATES) Log.d(TAG, "INTENT_VALUE_ICC_LOCKED and keygaurd isn't " + "showing; need to show keyguard so user can enter sim pin"); doKeyguardLocked(null); } else { - mPendingPinLock = true; resetStateLocked(); } } @@ -2991,6 +2991,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, pw.print(" mPendingReset: "); pw.println(mPendingReset); pw.print(" mPendingLock: "); pw.println(mPendingLock); pw.print(" wakeAndUnlocking: "); pw.println(mWakeAndUnlocking); + pw.print(" mPendingPinLock: "); pw.println(mPendingPinLock); } /** diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt new file mode 100644 index 000000000000..e3649187b0a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.lifecycle + +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.MainThread +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.lifecycleScope +import com.android.systemui.util.Assert +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.launch + +/** + * Runs the given [block] every time the [View] becomes attached (or immediately after calling this + * function, if the view was already attached), automatically canceling the work when the `View` + * becomes detached. + * + * Only use from the main thread. + * + * When [block] is run, it is run in the context of a [ViewLifecycleOwner] which the caller can use + * to launch jobs, with confidence that the jobs will be properly canceled when the view is + * detached. + * + * The [block] may be run multiple times, running once per every time the view is attached. Each + * time the block is run for a new attachment event, the [ViewLifecycleOwner] provided will be a + * fresh one. + * + * @param coroutineContext An optional [CoroutineContext] to replace the dispatcher [block] is + * invoked on. + * @param block The block of code that should be run when the view becomes attached. It can end up + * being invoked multiple times if the view is reattached after being detached. + * @return A [DisposableHandle] to invoke when the caller of the function destroys its [View] and is + * no longer interested in the [block] being run the next time its attached. Calling this is an + * optional optimization as the logic will be properly cleaned up and destroyed each time the view + * is detached. Using this is not *thread-safe* and should only be used on the main thread. + */ +@MainThread +fun View.repeatWhenAttached( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + block: suspend LifecycleOwner.(View) -> Unit, +): DisposableHandle { + Assert.isMainThread() + val view = this + // The suspend block will run on the app's main thread unless the caller supplies a different + // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as + // default behavior. Instead, we want it to run on the view's UI thread since the user will + // presumably want to call view methods that require being called from said UI thread. + val lifecycleCoroutineContext = Dispatchers.Main + coroutineContext + var lifecycleOwner: ViewLifecycleOwner? = null + val onAttachListener = + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) { + Assert.isMainThread() + lifecycleOwner?.onDestroy() + lifecycleOwner = + createLifecycleOwnerAndRun( + view, + lifecycleCoroutineContext, + block, + ) + } + + override fun onViewDetachedFromWindow(v: View?) { + lifecycleOwner?.onDestroy() + lifecycleOwner = null + } + } + + addOnAttachStateChangeListener(onAttachListener) + if (view.isAttachedToWindow) { + lifecycleOwner = + createLifecycleOwnerAndRun( + view, + lifecycleCoroutineContext, + block, + ) + } + + return object : DisposableHandle { + override fun dispose() { + Assert.isMainThread() + + lifecycleOwner?.onDestroy() + lifecycleOwner = null + view.removeOnAttachStateChangeListener(onAttachListener) + } + } +} + +private fun createLifecycleOwnerAndRun( + view: View, + coroutineContext: CoroutineContext, + block: suspend LifecycleOwner.(View) -> Unit, +): ViewLifecycleOwner { + return ViewLifecycleOwner(view).apply { + onCreate() + lifecycleScope.launch(coroutineContext) { block(view) } + } +} + +/** + * A [LifecycleOwner] for a [View] for exclusive use by the [repeatWhenAttached] extension function. + * + * The implementation requires the caller to call [onCreate] and [onDestroy] when the view is + * attached to or detached from a view hierarchy. After [onCreate] and before [onDestroy] is called, + * the implementation monitors window state in the following way + * + * * If the window is not visible, we are in the [Lifecycle.State.CREATED] state + * * If the window is visible but not focused, we are in the [Lifecycle.State.STARTED] state + * * If the window is visible and focused, we are in the [Lifecycle.State.RESUMED] state + * + * Or in table format: + * ``` + * ┌───────────────┬───────────────────┬──────────────┬─────────────────┐ + * │ View attached │ Window Visibility │ Window Focus │ Lifecycle State │ + * ├───────────────┼───────────────────┴──────────────┼─────────────────┤ + * │ Not attached │ Any │ N/A │ + * ├───────────────┼───────────────────┬──────────────┼─────────────────┤ + * │ │ Not visible │ Any │ CREATED │ + * │ ├───────────────────┼──────────────┼─────────────────┤ + * │ Attached │ │ No focus │ STARTED │ + * │ │ Visible ├──────────────┼─────────────────┤ + * │ │ │ Has focus │ RESUMED │ + * └───────────────┴───────────────────┴──────────────┴─────────────────┘ + * ``` + */ +private class ViewLifecycleOwner( + private val view: View, +) : LifecycleOwner { + + private val windowVisibleListener = + ViewTreeObserver.OnWindowVisibilityChangeListener { updateState() } + private val windowFocusListener = ViewTreeObserver.OnWindowFocusChangeListener { updateState() } + + private val registry = LifecycleRegistry(this) + + fun onCreate() { + registry.currentState = Lifecycle.State.CREATED + view.viewTreeObserver.addOnWindowVisibilityChangeListener(windowVisibleListener) + view.viewTreeObserver.addOnWindowFocusChangeListener(windowFocusListener) + updateState() + } + + fun onDestroy() { + view.viewTreeObserver.removeOnWindowVisibilityChangeListener(windowVisibleListener) + view.viewTreeObserver.removeOnWindowFocusChangeListener(windowFocusListener) + registry.currentState = Lifecycle.State.DESTROYED + } + + override fun getLifecycle(): Lifecycle { + return registry + } + + private fun updateState() { + registry.currentState = + when { + view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED + !view.hasWindowFocus() -> Lifecycle.State.STARTED + else -> Lifecycle.State.RESUMED + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt deleted file mode 100644 index 55c7ac9fb0cc..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwner.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.android.systemui.lifecycle - -import android.view.View -import android.view.ViewTreeObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry - -/** - * [LifecycleOwner] for Window-added Views. - * - * These are [View] instances that are added to a `Window` using the `WindowManager` API. - * - * This implementation goes to: - * * The <b>CREATED</b> `Lifecycle.State` when the view gets attached to the window but the window - * is not yet visible - * * The <b>STARTED</b> `Lifecycle.State` when the view is attached to the window and the window is - * visible - * * The <b>RESUMED</b> `Lifecycle.State` when the view is attached to the window and the window is - * visible and the window receives focus - * - * In table format: - * ``` - * | ----------------------------------------------------------------------------- | - * | View attached to window | Window visible | Window has focus | Lifecycle state | - * | ----------------------------------------------------------------------------- | - * | not attached | Any | INITIALIZED | - * | ----------------------------------------------------------------------------- | - * | | not visible | Any | CREATED | - * | ----------------------------------------------------- | - * | attached | | not focused | STARTED | - * | | is visible |----------------------------------- | - * | | | has focus | RESUMED | - * | ----------------------------------------------------------------------------- | - * ``` - * ### Notes - * * [dispose] must be invoked when the [LifecycleOwner] is done and won't be reused - * * It is always better for [LifecycleOwner] implementations to be more explicit than just - * listening to the state of the `Window`. E.g. if the code that added the `View` to the `Window` - * already has access to the correct state to know when that `View` should become visible and when - * it is ready to receive interaction from the user then it already knows when to move to `STARTED` - * and `RESUMED`, respectively. In that case, it's better to implement your own `LifecycleOwner` - * instead of relying on the `Window` callbacks. - */ -class WindowAddedViewLifecycleOwner -@JvmOverloads -constructor( - private val view: View, - registryFactory: (LifecycleOwner) -> LifecycleRegistry = { LifecycleRegistry(it) }, -) : LifecycleOwner { - - private val windowAttachListener = - object : ViewTreeObserver.OnWindowAttachListener { - override fun onWindowAttached() { - updateCurrentState() - } - - override fun onWindowDetached() { - updateCurrentState() - } - } - private val windowFocusListener = - ViewTreeObserver.OnWindowFocusChangeListener { updateCurrentState() } - private val windowVisibilityListener = - ViewTreeObserver.OnWindowVisibilityChangeListener { updateCurrentState() } - - private val registry = registryFactory(this) - - init { - setCurrentState(Lifecycle.State.INITIALIZED) - - with(view.viewTreeObserver) { - addOnWindowAttachListener(windowAttachListener) - addOnWindowVisibilityChangeListener(windowVisibilityListener) - addOnWindowFocusChangeListener(windowFocusListener) - } - - updateCurrentState() - } - - override fun getLifecycle(): Lifecycle { - return registry - } - - /** - * Disposes of this [LifecycleOwner], performing proper clean-up. - * - * <p>Invoke this when the instance is finished and won't be reused. - */ - fun dispose() { - with(view.viewTreeObserver) { - removeOnWindowAttachListener(windowAttachListener) - removeOnWindowVisibilityChangeListener(windowVisibilityListener) - removeOnWindowFocusChangeListener(windowFocusListener) - } - } - - private fun updateCurrentState() { - val state = - when { - !view.isAttachedToWindow -> Lifecycle.State.INITIALIZED - view.windowVisibility != View.VISIBLE -> Lifecycle.State.CREATED - !view.hasWindowFocus() -> Lifecycle.State.STARTED - else -> Lifecycle.State.RESUMED - } - setCurrentState(state) - } - - private fun setCurrentState(state: Lifecycle.State) { - if (registry.currentState != state) { - registry.currentState = state - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt index db446c39767a..dc23684dd517 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt @@ -20,14 +20,19 @@ import android.os.Trace import android.util.Log import com.android.systemui.log.dagger.LogModule import com.android.systemui.util.collection.RingBuffer +import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.text.SimpleDateFormat +import java.util.Arrays.stream import java.util.Locale import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.BlockingQueue import kotlin.concurrent.thread import kotlin.math.max +const val UNBOUNDED_STACK_TRACE = -1 +const val NESTED_TRACE_DEPTH = 10 + /** * A simple ring buffer of recyclable log messages * @@ -69,12 +74,18 @@ import kotlin.math.max * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches * the maximum, it behaves like a ring buffer. + * @param rootStackTraceDepth The number of stack trace elements to be logged for an exception when + * the logBuffer is dumped. Defaulted to -1 [UNBOUNDED_STACK_TRACE] to print the entire stack trace. + * @param nestedStackTraceDepth The number of stack trace elements to be logged for any nested + * exceptions present in [Throwable.cause] or [Throwable.suppressedExceptions]. */ class LogBuffer @JvmOverloads constructor( private val name: String, private val maxSize: Int, private val logcatEchoTracker: LogcatEchoTracker, - private val systrace: Boolean = true + private val systrace: Boolean = true, + private val rootStackTraceDepth: Int = UNBOUNDED_STACK_TRACE, + private val nestedStackTraceDepth: Int = NESTED_TRACE_DEPTH, ) { private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() } @@ -107,11 +118,11 @@ class LogBuffer @JvmOverloads constructor( * May also log the message to logcat if echoing is enabled for this buffer or tag. * * The actual string of the log message is not constructed until it is needed. To accomplish - * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is - * obtained and is passed to the [initializer]. The initializer stores any relevant data on the - * message's fields. The message is then inserted into the buffer where it waits until it is - * either pushed out by newer messages or it needs to printed. If and when this latter moment - * occurs, the [printer] function is called on the message. It reads whatever data the + * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is + * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data + * on the message's fields. The message is then inserted into the buffer where it waits until it + * is either pushed out by newer messages or it needs to printed. If and when this latter moment + * occurs, the [messagePrinter] function is called on the message. It reads whatever data the * initializer stored and converts it to a human-readable log message. * * @param tag A string of at most 23 characters, used for grouping logs into categories or @@ -120,27 +131,49 @@ class LogBuffer @JvmOverloads constructor( * echoed. In general, a module should split most of its logs into either INFO or DEBUG level. * INFO level should be reserved for information that other parts of the system might care * about, leaving the specifics of code's day-to-day operations to DEBUG. - * @param initializer A function that will be called immediately to store relevant data on the - * log message. The value of `this` will be the LogMessage to be initialized. - * @param printer A function that will be called if and when the message needs to be dumped to - * logcat or a bug report. It should read the data stored by the initializer and convert it to - * a human-readable string. The value of `this` will be the LogMessage to be printed. - * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any - * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance - * of the printer for each call, thwarting our attempts at avoiding any sort of allocation. + * @param messageInitializer A function that will be called immediately to store relevant data + * on the log message. The value of `this` will be the LogMessage to be initialized. + * @param messagePrinter A function that will be called if and when the message needs to be + * dumped to logcat or a bug report. It should read the data stored by the initializer and + * convert it to a human-readable string. The value of `this` will be the LogMessage to be + * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and + * NEVER any variables in its enclosing scope. Otherwise, the runtime will need to allocate a + * new instance of the printer for each call, thwarting our attempts at avoiding any sort of + * allocation. + * @param exception Provide any exception that need to be logged. This is saved as + * [LogMessage.exception] */ + @JvmOverloads inline fun log( - tag: String, - level: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String + tag: String, + level: LogLevel, + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, ) { - val message = obtain(tag, level, printer) - initializer(message) + val message = obtain(tag, level, messagePrinter, exception) + messageInitializer(message) commit(message) } /** + * Logs a compile-time string constant [message] to the log buffer. Use sparingly. + * + * May also log the message to logcat if echoing is enabled for this buffer or tag. This is for + * simpler use-cases where [message] is a compile time string constant. For use-cases where the + * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in + * an initializer and a message printer. + * + * Log buffers are limited by the number of entries, so logging more frequently + * will limit the time window that the LogBuffer covers in a bug report. Richer logs, on the + * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to + * add more detail to every log may do more to improve overall logging than adding more logs + * with this method. + */ + fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = + log(tag, level, {str1 = message}, { str1!! }) + + /** * You should call [log] instead of this method. * * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size, @@ -151,15 +184,16 @@ class LogBuffer @JvmOverloads constructor( */ @Synchronized fun obtain( - tag: String, - level: LogLevel, - printer: (LogMessage) -> String - ): LogMessageImpl { + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable? = null, + ): LogMessage { if (!mutable) { return FROZEN_MESSAGE } val message = buffer.advance() - message.reset(tag, level, System.currentTimeMillis(), printer) + message.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception) return message } @@ -230,19 +264,79 @@ class LogBuffer @JvmOverloads constructor( } } - private fun dumpMessage(message: LogMessage, pw: PrintWriter) { - pw.print(DATE_FORMAT.format(message.timestamp)) + private fun dumpMessage( + message: LogMessage, + pw: PrintWriter + ) { + val formattedTimestamp = DATE_FORMAT.format(message.timestamp) + val shortLevel = message.level.shortString + val messageToPrint = message.messagePrinter(message) + val tag = message.tag + printLikeLogcat(pw, formattedTimestamp, shortLevel, tag, messageToPrint) + message.exception?.let { ex -> + printException( + pw, + formattedTimestamp, + shortLevel, + ex, + tag, + stackTraceDepth = rootStackTraceDepth) + } + } + + private fun printException( + pw: PrintWriter, + timestamp: String, + level: String, + exception: Throwable, + tag: String, + exceptionMessagePrefix: String = "", + stackTraceDepth: Int = UNBOUNDED_STACK_TRACE + ) { + val message = "$exceptionMessagePrefix$exception" + printLikeLogcat(pw, timestamp, level, tag, message) + var stacktraceStream = stream(exception.stackTrace) + if (stackTraceDepth != UNBOUNDED_STACK_TRACE) { + stacktraceStream = stacktraceStream.limit(stackTraceDepth.toLong()) + } + stacktraceStream.forEach { line -> + printLikeLogcat(pw, timestamp, level, tag, "\tat $line") + } + exception.cause?.let { cause -> + printException(pw, timestamp, level, cause, tag, "Caused by: ", nestedStackTraceDepth) + } + exception.suppressedExceptions.forEach { suppressed -> + printException( + pw, + timestamp, + level, + suppressed, + tag, + "Suppressed: ", + nestedStackTraceDepth + ) + } + } + + private fun printLikeLogcat( + pw: PrintWriter, + formattedTimestamp: String, + shortLogLevel: String, + tag: String, + message: String + ) { + pw.print(formattedTimestamp) pw.print(" ") - pw.print(message.level.shortString) + pw.print(shortLogLevel) pw.print(" ") - pw.print(message.tag) + pw.print(tag) pw.print(": ") - pw.println(message.printer(message)) + pw.println(message) } private fun echo(message: LogMessage, toLogcat: Boolean, toSystrace: Boolean) { if (toLogcat || toSystrace) { - val strMessage = message.printer(message) + val strMessage = message.messagePrinter(message) if (toSystrace) { echoToSystrace(message, strMessage) } @@ -259,16 +353,22 @@ class LogBuffer @JvmOverloads constructor( private fun echoToLogcat(message: LogMessage, strMessage: String) { when (message.level) { - LogLevel.VERBOSE -> Log.v(message.tag, strMessage) - LogLevel.DEBUG -> Log.d(message.tag, strMessage) - LogLevel.INFO -> Log.i(message.tag, strMessage) - LogLevel.WARNING -> Log.w(message.tag, strMessage) - LogLevel.ERROR -> Log.e(message.tag, strMessage) - LogLevel.WTF -> Log.wtf(message.tag, strMessage) + LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception) + LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception) + LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception) + LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception) + LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception) + LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception) } } } +/** + * A function that will be called immediately to store relevant data on the log message. The value + * of `this` will be the LogMessage to be initialized. + */ +typealias MessageInitializer = LogMessage.() -> Unit + private const val TAG = "LogBuffer" private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) -private val FROZEN_MESSAGE = LogMessageImpl.create()
\ No newline at end of file +private val FROZEN_MESSAGE = LogMessageImpl.create() diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt index 2a0a2aa6fb38..987aea8bff08 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt @@ -25,7 +25,7 @@ package com.android.systemui.log * * When a message is logged, the code doing the logging stores data in one or more of the generic * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the - * [printer] function reads the data stored in the generic fields and converts that to a human- + * [messagePrinter] function reads the data stored in the generic fields and converts that to a human- * readable string. Thus, for every log type there must be a specialized initializer function that * stores data specific to that log type and a specialized printer function that prints that data. * @@ -35,7 +35,8 @@ interface LogMessage { val level: LogLevel val tag: String val timestamp: Long - val printer: LogMessage.() -> String + val messagePrinter: MessagePrinter + val exception: Throwable? var str1: String? var str2: String? @@ -50,3 +51,13 @@ interface LogMessage { var bool3: Boolean var bool4: Boolean } + +/** + * A function that will be called if and when the message needs to be dumped to + * logcat or a bug report. It should read the data stored by the initializer and convert it to + * a human-readable string. The value of `this` will be the LogMessage to be printed. + * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any + * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance + * of the printer for each call, thwarting our attempts at avoiding any sort of allocation. + */ +typealias MessagePrinter = LogMessage.() -> String diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt index d33ac4b4a80b..4dd6f652d1c7 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt @@ -23,7 +23,8 @@ data class LogMessageImpl( override var level: LogLevel, override var tag: String, override var timestamp: Long, - override var printer: LogMessage.() -> String, + override var messagePrinter: MessagePrinter, + override var exception: Throwable?, override var str1: String?, override var str2: String?, override var str3: String?, @@ -35,19 +36,21 @@ data class LogMessageImpl( override var bool1: Boolean, override var bool2: Boolean, override var bool3: Boolean, - override var bool4: Boolean + override var bool4: Boolean, ) : LogMessage { fun reset( tag: String, level: LogLevel, timestamp: Long, - renderer: LogMessage.() -> String + renderer: MessagePrinter, + exception: Throwable? = null, ) { this.level = level this.tag = tag this.timestamp = timestamp - this.printer = renderer + this.messagePrinter = renderer + this.exception = exception str1 = null str2 = null str3 = null @@ -68,7 +71,8 @@ data class LogMessageImpl( LogLevel.DEBUG, DEFAULT_TAG, 0, - DEFAULT_RENDERER, + DEFAULT_PRINTER, + null, null, null, null, @@ -86,4 +90,4 @@ data class LogMessageImpl( } private const val DEFAULT_TAG = "UnknownTag" -private val DEFAULT_RENDERER: LogMessage.() -> String = { "Unknown message: $this" } +private val DEFAULT_PRINTER: MessagePrinter = { "Unknown message: $this" } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 4b71b2c94f5f..15e1129e43b5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -44,8 +44,6 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.WindowManagerGlobal; -import androidx.lifecycle.ViewTreeLifecycleOwner; - import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; import com.android.systemui.R; @@ -54,7 +52,6 @@ import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardViewMediator; -import com.android.systemui.lifecycle.WindowAddedViewLifecycleOwner; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -251,15 +248,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mWindowManager.addView(mNotificationShadeView, mLp); - // Set up and "inject" a LifecycleOwner bound to the Window-View relationship such that all - // views in the sub-tree rooted under this view can access the LifecycleOwner using - // ViewTreeLifecycleOwner.get(...). - if (ViewTreeLifecycleOwner.get(mNotificationShadeView) == null) { - ViewTreeLifecycleOwner.set( - mNotificationShadeView, - new WindowAddedViewLifecycleOwner(mNotificationShadeView)); - } - mLpChanged.copyFrom(mLp); onThemeChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index 945bc72e3e81..9e5dab1152ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -68,9 +68,6 @@ data class ListAttachState private constructor( */ var stableIndex: Int = -1 - /** Access the index of the [section] or -1 if the entry does not have one */ - val sectionIndex: Int get() = section?.index ?: -1 - /** Copies the state of another instance. */ fun clone(other: ListAttachState) { parent = other.parent diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 5198d82a86c7..075a0dc7555e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -1039,25 +1039,22 @@ public class ShadeListBuilder implements Dumpable { NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); int sectionMemberIndex = 0; for (int i = 0; i < notifList.size(); i++) { - final ListEntry entry = notifList.get(i); + ListEntry entry = notifList.get(i); NotifSection section = requireNonNull(entry.getSection()); if (section.getIndex() != currentSection.getIndex()) { sectionMemberIndex = 0; currentSection = section; } - entry.getAttachState().setStableIndex(sectionMemberIndex++); + entry.getAttachState().setStableIndex(sectionMemberIndex); if (entry instanceof GroupEntry) { - final GroupEntry parent = (GroupEntry) entry; - final NotificationEntry summary = parent.getSummary(); - if (summary != null) { - summary.getAttachState().setStableIndex(sectionMemberIndex++); - } - final List<NotificationEntry> children = parent.getChildren(); - for (int j = 0; j < children.size(); j++) { - final NotificationEntry child = children.get(j); - child.getAttachState().setStableIndex(sectionMemberIndex++); + GroupEntry parent = (GroupEntry) entry; + for (int j = 0; j < parent.getChildren().size(); j++) { + entry = parent.getChildren().get(j); + entry.getAttachState().setStableIndex(sectionMemberIndex); + sectionMemberIndex++; } } + sectionMemberIndex++; } } @@ -1197,9 +1194,9 @@ public class ShadeListBuilder implements Dumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; - cmp = Integer.compare( - getStableOrderIndex(o1), - getStableOrderIndex(o2)); + int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex(); + int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex(); + cmp = Integer.compare(index1, index2); if (cmp != 0) return cmp; NotifComparator sectionComparator = getSectionComparator(o1, o2); @@ -1213,32 +1210,31 @@ public class ShadeListBuilder implements Dumpable { if (cmp != 0) return cmp; } - cmp = Integer.compare( - o1.getRepresentativeEntry().getRanking().getRank(), - o2.getRepresentativeEntry().getRanking().getRank()); + final NotificationEntry rep1 = o1.getRepresentativeEntry(); + final NotificationEntry rep2 = o2.getRepresentativeEntry(); + cmp = rep1.getRanking().getRank() - rep2.getRanking().getRank(); if (cmp != 0) return cmp; - cmp = -1 * Long.compare( - o1.getRepresentativeEntry().getSbn().getNotification().when, - o2.getRepresentativeEntry().getSbn().getNotification().when); + cmp = Long.compare( + rep2.getSbn().getNotification().when, + rep1.getSbn().getNotification().when); return cmp; }; private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { - int cmp = Integer.compare( - getStableOrderIndex(o1), - getStableOrderIndex(o2)); + int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex(); + int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex(); + int cmp = Integer.compare(index1, index2); if (cmp != 0) return cmp; - cmp = Integer.compare( - o1.getRepresentativeEntry().getRanking().getRank(), - o2.getRepresentativeEntry().getRanking().getRank()); + cmp = o1.getRepresentativeEntry().getRanking().getRank() + - o2.getRepresentativeEntry().getRanking().getRank(); if (cmp != 0) return cmp; - cmp = -1 * Long.compare( - o1.getRepresentativeEntry().getSbn().getNotification().when, - o2.getRepresentativeEntry().getSbn().getNotification().when); + cmp = Long.compare( + o2.getRepresentativeEntry().getSbn().getNotification().when, + o1.getRepresentativeEntry().getSbn().getNotification().when); return cmp; }; @@ -1248,21 +1244,8 @@ public class ShadeListBuilder implements Dumpable { */ private boolean mForceReorderable = false; - private int getStableOrderIndex(ListEntry entry) { - if (mForceReorderable) { - // this is used to determine if the list is correctly sorted - return -1; - } - if (getStabilityManager().isEntryReorderingAllowed(entry)) { - // let the stability manager constrain or allow reordering - return -1; - } - if (entry.getAttachState().getSectionIndex() - != entry.getPreviousAttachState().getSectionIndex()) { - // stable index is only valid within the same section; otherwise we allow reordering - return -1; - } - return entry.getPreviousAttachState().getStableIndex(); + private boolean canReorder(ListEntry entry) { + return mForceReorderable || getStabilityManager().isEntryReorderingAllowed(entry); } private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt index 4c406e3ba0b4..d8dae5d23f42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt @@ -336,7 +336,7 @@ class ShadeListBuilderLogger @Inject constructor( } fun logPipelineRunSuppressed() = - buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." } + buffer.log(TAG, INFO, {}, { "Suppressing pipeline run during animation." }) } private const val TAG = "ShadeListBuilder" diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 3e07144dce74..e22a896227ef 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -40,7 +40,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.ZenModeConfig; -import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -83,6 +82,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -262,7 +262,7 @@ public class BubblesManager implements Dumpable { } @Override - public void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys, + public void getShouldRestoredEntries(Set<String> savedBubbleKeys, Consumer<List<BubbleEntry>> callback) { sysuiMainExecutor.execute(() -> { List<BubbleEntry> result = new ArrayList<>(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 9b665555562a..21c018a0419d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -186,6 +186,31 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void restoreBouncerWhenSimLockedAndKeyguardIsGoingAway_initiallyNotShowing() { + // When showing and provisioned + mViewMediator.onSystemReady(); + when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); + mViewMediator.setShowingLocked(false); + + // and a SIM becomes locked and requires a PIN + mViewMediator.mUpdateCallback.onSimStateChanged( + 1 /* subId */, + 0 /* slotId */, + TelephonyManager.SIM_STATE_PIN_REQUIRED); + + // and the keyguard goes away + mViewMediator.setShowingLocked(false); + when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); + + TestableLooper.get(this).processAllMessages(); + + // then make sure it comes back + verify(mStatusBarKeyguardViewManager, atLeast(1)).show(null); + } + + @Test public void testBouncerPrompt_deviceLockedByAdmin() { // GIVEN no trust agents enabled and biometrics aren't enrolled when(mUpdateMonitor.isTrustUsuallyManaged(anyInt())).thenReturn(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt new file mode 100644 index 000000000000..80f3e46b848f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.lifecycle + +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import android.view.ViewTreeObserver +import androidx.arch.core.executor.ArchTaskExecutor +import androidx.arch.core.executor.TaskExecutor +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.Assert +import com.android.systemui.util.mockito.argumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(JUnit4::class) +@RunWithLooper +class RepeatWhenAttachedTest : SysuiTestCase() { + + @JvmField @Rule val mockito = MockitoJUnit.rule() + @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() + + @Mock private lateinit var view: View + @Mock private lateinit var viewTreeObserver: ViewTreeObserver + + private lateinit var block: Block + private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener> + + @Before + fun setUp() { + Assert.setTestThread(Thread.currentThread()) + whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) + whenever(view.windowVisibility).thenReturn(View.GONE) + whenever(view.hasWindowFocus()).thenReturn(false) + attachListeners = mutableListOf() + whenever(view.addOnAttachStateChangeListener(any())).then { + attachListeners.add(it.arguments[0] as View.OnAttachStateChangeListener) + } + whenever(view.removeOnAttachStateChangeListener(any())).then { + attachListeners.remove(it.arguments[0] as View.OnAttachStateChangeListener) + } + block = Block() + } + + @Test(expected = IllegalStateException::class) + fun `repeatWhenAttached - enforces main thread`() = runBlockingTest { + Assert.setTestThread(null) + + repeatWhenAttached() + } + + @Test(expected = IllegalStateException::class) + fun `repeatWhenAttached - dispose enforces main thread`() = runBlockingTest { + val disposableHandle = repeatWhenAttached() + Assert.setTestThread(null) + + disposableHandle.dispose() + } + + @Test + fun `repeatWhenAttached - view starts detached - runs block when attached`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(false) + repeatWhenAttached() + assertThat(block.invocationCount).isEqualTo(0) + + whenever(view.isAttachedToWindow).thenReturn(true) + attachListeners.last().onViewAttachedToWindow(view) + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun `repeatWhenAttached - view already attached - immediately runs block`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + + repeatWhenAttached() + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun `repeatWhenAttached - starts visible without focus - STARTED`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + + repeatWhenAttached() + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) + } + + @Test + fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.hasWindowFocus()).thenReturn(true) + + repeatWhenAttached() + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + whenever(view.hasWindowFocus()).thenReturn(true) + + repeatWhenAttached() + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun `repeatWhenAttached - becomes visible without focus - STARTED`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() + verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) + + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) + } + + @Test + fun `repeatWhenAttached - gains focus but invisible - CREATED`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() + verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) + + whenever(view.hasWindowFocus()).thenReturn(true) + listenerCaptor.value.onWindowFocusChanged(true) + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() + verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture()) + val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() + verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) + + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + whenever(view.hasWindowFocus()).thenReturn(true) + focusCaptor.value.onWindowFocusChanged(true) + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + + whenever(view.isAttachedToWindow).thenReturn(false) + attachListeners.last().onViewDetachedFromWindow(view) + + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } + + @Test + fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + whenever(view.isAttachedToWindow).thenReturn(false) + attachListeners.last().onViewDetachedFromWindow(view) + + whenever(view.isAttachedToWindow).thenReturn(true) + attachListeners.last().onViewAttachedToWindow(view) + + assertThat(block.invocationCount).isEqualTo(2) + assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun `repeatWhenAttached - dispose attached`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + val handle = repeatWhenAttached() + + handle.dispose() + + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } + + @Test + fun `repeatWhenAttached - dispose never attached`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(false) + val handle = repeatWhenAttached() + + handle.dispose() + + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(0) + } + + @Test + fun `repeatWhenAttached - dispose previously attached now detached`() = runBlockingTest { + whenever(view.isAttachedToWindow).thenReturn(true) + val handle = repeatWhenAttached() + attachListeners.last().onViewDetachedFromWindow(view) + + handle.dispose() + + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } + + private fun CoroutineScope.repeatWhenAttached(): DisposableHandle { + return view.repeatWhenAttached( + coroutineContext = coroutineContext, + block = block, + ) + } + + private class Block : suspend LifecycleOwner.(View) -> Unit { + data class Invocation( + val lifecycleOwner: LifecycleOwner, + ) { + val lifecycleState: Lifecycle.State + get() = lifecycleOwner.lifecycle.currentState + } + + private val _invocations = mutableListOf<Invocation>() + val invocations: List<Invocation> = _invocations + val invocationCount: Int + get() = _invocations.size + val latestLifecycleState: Lifecycle.State + get() = _invocations.last().lifecycleState + + override suspend fun invoke(lifecycleOwner: LifecycleOwner, view: View) { + _invocations.add(Invocation(lifecycleOwner)) + } + } + + /** + * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert + * in LifecycleRegistry. + */ + class InstantTaskExecutorRule : TestWatcher() { + // TODO(b/240620122): This is a copy of + // androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be replaced + // with a dependency on the real library once b/ is cleared. + override fun starting(description: Description) { + super.starting(description) + ArchTaskExecutor.getInstance() + .setDelegate( + object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) { + runnable.run() + } + + override fun postToMainThread(runnable: Runnable) { + runnable.run() + } + + override fun isMainThread(): Boolean { + return true + } + } + ) + } + + override fun finished(description: Description) { + super.finished(description) + ArchTaskExecutor.getInstance().setDelegate(null) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt deleted file mode 100644 index 4f5c570ee812..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/WindowAddedViewLifecycleOwnerTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.android.systemui.lifecycle - -import android.view.View -import android.view.ViewTreeObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleRegistry -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(JUnit4::class) -class WindowAddedViewLifecycleOwnerTest : SysuiTestCase() { - - @Mock lateinit var view: View - @Mock lateinit var viewTreeObserver: ViewTreeObserver - - private lateinit var underTest: WindowAddedViewLifecycleOwner - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) - whenever(view.isAttachedToWindow).thenReturn(false) - whenever(view.windowVisibility).thenReturn(View.INVISIBLE) - whenever(view.hasWindowFocus()).thenReturn(false) - - underTest = WindowAddedViewLifecycleOwner(view) { LifecycleRegistry.createUnsafe(it) } - } - - @Test - fun `detached - invisible - does not have focus -- INITIALIZED`() { - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) - } - - @Test - fun `detached - invisible - has focus -- INITIALIZED`() { - whenever(view.hasWindowFocus()).thenReturn(true) - val captor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(captor)) - captor.value.onWindowFocusChanged(true) - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) - } - - @Test - fun `detached - visible - does not have focus -- INITIALIZED`() { - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - val captor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(captor)) - captor.value.onWindowVisibilityChanged(View.VISIBLE) - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) - } - - @Test - fun `detached - visible - has focus -- INITIALIZED`() { - whenever(view.hasWindowFocus()).thenReturn(true) - val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(focusCaptor)) - focusCaptor.value.onWindowFocusChanged(true) - - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - val visibilityCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(visibilityCaptor)) - visibilityCaptor.value.onWindowVisibilityChanged(View.VISIBLE) - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) - } - - @Test - fun `attached - invisible - does not have focus -- CREATED`() { - whenever(view.isAttachedToWindow).thenReturn(true) - val captor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>() - verify(viewTreeObserver).addOnWindowAttachListener(capture(captor)) - captor.value.onWindowAttached() - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) - } - - @Test - fun `attached - invisible - has focus -- CREATED`() { - whenever(view.isAttachedToWindow).thenReturn(true) - val attachCaptor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>() - verify(viewTreeObserver).addOnWindowAttachListener(capture(attachCaptor)) - attachCaptor.value.onWindowAttached() - - whenever(view.hasWindowFocus()).thenReturn(true) - val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(focusCaptor)) - focusCaptor.value.onWindowFocusChanged(true) - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) - } - - @Test - fun `attached - visible - does not have focus -- STARTED`() { - whenever(view.isAttachedToWindow).thenReturn(true) - val attachCaptor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>() - verify(viewTreeObserver).addOnWindowAttachListener(capture(attachCaptor)) - attachCaptor.value.onWindowAttached() - - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - val visibilityCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(visibilityCaptor)) - visibilityCaptor.value.onWindowVisibilityChanged(View.VISIBLE) - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) - } - - @Test - fun `attached - visible - has focus -- RESUMED`() { - whenever(view.isAttachedToWindow).thenReturn(true) - val attachCaptor = argumentCaptor<ViewTreeObserver.OnWindowAttachListener>() - verify(viewTreeObserver).addOnWindowAttachListener(capture(attachCaptor)) - attachCaptor.value.onWindowAttached() - - whenever(view.hasWindowFocus()).thenReturn(true) - val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(capture(focusCaptor)) - focusCaptor.value.onWindowFocusChanged(true) - - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - val visibilityCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(capture(visibilityCaptor)) - visibilityCaptor.value.onWindowVisibilityChanged(View.VISIBLE) - - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) - } - - @Test - fun dispose() { - underTest.dispose() - - verify(viewTreeObserver).removeOnWindowAttachListener(any()) - verify(viewTreeObserver).removeOnWindowVisibilityChangeListener(any()) - verify(viewTreeObserver).removeOnWindowFocusChangeListener(any()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt new file mode 100644 index 000000000000..4abb973817b1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt @@ -0,0 +1,167 @@ +package com.android.systemui.log + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner + +@SmallTest +@RunWith(MockitoJUnitRunner::class) +class LogBufferTest : SysuiTestCase() { + private lateinit var buffer: LogBuffer + + private lateinit var outputWriter: StringWriter + + @Mock + private lateinit var logcatEchoTracker: LogcatEchoTracker + + @Before + fun setup() { + outputWriter = StringWriter() + buffer = createBuffer(UNBOUNDED_STACK_TRACE, NESTED_TRACE_DEPTH) + } + + private fun createBuffer(rootTraceDepth: Int, nestedTraceDepth: Int): LogBuffer { + return LogBuffer("TestBuffer", + 1, + logcatEchoTracker, + false, + rootStackTraceDepth = rootTraceDepth, + nestedStackTraceDepth = nestedTraceDepth) + } + + @Test + fun log_shouldSaveLogToBuffer() { + buffer.log("Test", LogLevel.INFO, "Some test message") + + val dumpedString = dumpBuffer() + + assertThat(dumpedString).contains("Some test message") + } + + @Test + fun log_shouldRotateIfLogBufferIsFull() { + buffer.log("Test", LogLevel.INFO, "This should be rotated") + buffer.log("Test", LogLevel.INFO, "New test message") + + val dumpedString = dumpBuffer() + + assertThat(dumpedString).contains("New test message") + } + + @Test + fun dump_writesExceptionAndStacktraceLimitedToGivenDepth() { + buffer = createBuffer(rootTraceDepth = 2, nestedTraceDepth = -1) + // stack trace depth of 5 + val exception = createTestException("Exception message", "TestClass", 5) + buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) + + val dumpedString = dumpBuffer() + + // logs are limited to depth 2 + assertThat(dumpedString).contains("E Tag: Extra message") + assertThat(dumpedString).contains("E Tag: java.lang.RuntimeException: Exception message") + assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:1)") + assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:2)") + assertThat(dumpedString) + .doesNotContain("E Tag: \tat TestClass.TestMethod(TestClass.java:3)") + } + + @Test + fun dump_writesCauseAndStacktraceLimitedToGivenDepth() { + buffer = createBuffer(rootTraceDepth = 0, nestedTraceDepth = 2) + val exception = createTestException("Exception message", + "TestClass", + 1, + cause = createTestException("The real cause!", "TestClass", 5)) + buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) + + val dumpedString = dumpBuffer() + + // logs are limited to depth 2 + assertThat(dumpedString) + .contains("E Tag: Caused by: java.lang.RuntimeException: The real cause!") + assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:1)") + assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:2)") + assertThat(dumpedString) + .doesNotContain("E Tag: \tat TestClass.TestMethod(TestClass.java:3)") + } + + @Test + fun dump_writesSuppressedExceptionAndStacktraceLimitedToGivenDepth() { + buffer = createBuffer(rootTraceDepth = 0, nestedTraceDepth = 2) + val exception = RuntimeException("Root exception message") + exception.addSuppressed( + createTestException( + "First suppressed exception", + "FirstClass", + 5, + createTestException("Cause of suppressed exp", "ThirdClass", 5) + )) + exception.addSuppressed( + createTestException("Second suppressed exception", "SecondClass", 5)) + buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) + + val dumpedStr = dumpBuffer() + + // logs are limited to depth 2 + // first suppressed exception + assertThat(dumpedStr) + .contains("E Tag: Suppressed: " + + "java.lang.RuntimeException: First suppressed exception") + assertThat(dumpedStr).contains("E Tag: \tat FirstClass.TestMethod(FirstClass.java:1)") + assertThat(dumpedStr).contains("E Tag: \tat FirstClass.TestMethod(FirstClass.java:2)") + assertThat(dumpedStr) + .doesNotContain("E Tag: \tat FirstClass.TestMethod(FirstClass.java:3)") + + assertThat(dumpedStr) + .contains("E Tag: Caused by: java.lang.RuntimeException: Cause of suppressed exp") + assertThat(dumpedStr).contains("E Tag: \tat ThirdClass.TestMethod(ThirdClass.java:1)") + assertThat(dumpedStr).contains("E Tag: \tat ThirdClass.TestMethod(ThirdClass.java:2)") + assertThat(dumpedStr) + .doesNotContain("E Tag: \tat ThirdClass.TestMethod(ThirdClass.java:3)") + + // second suppressed exception + assertThat(dumpedStr) + .contains("E Tag: Suppressed: " + + "java.lang.RuntimeException: Second suppressed exception") + assertThat(dumpedStr).contains("E Tag: \tat SecondClass.TestMethod(SecondClass.java:1)") + assertThat(dumpedStr).contains("E Tag: \tat SecondClass.TestMethod(SecondClass.java:2)") + assertThat(dumpedStr) + .doesNotContain("E Tag: \tat SecondClass.TestMethod(SecondClass.java:3)") + } + + private fun createTestException( + message: String, + errorClass: String, + stackTraceLength: Int, + cause: Throwable? = null + ): Exception { + val exception = RuntimeException(message, cause) + exception.stackTrace = createStackTraceElements(errorClass, stackTraceLength) + return exception + } + + private fun dumpBuffer(): String { + buffer.dump(PrintWriter(outputWriter), tailLength = 100) + return outputWriter.toString() + } + + private fun createStackTraceElements( + errorClass: String, + stackTraceLength: Int + ): Array<StackTraceElement> { + return (1..stackTraceLength).map { lineNumber -> + StackTraceElement(errorClass, + "TestMethod", + "$errorClass.java", + lineNumber) + }.toTypedArray() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index ec1fa48662b4..ad3d3d2958cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -28,7 +28,6 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; @@ -44,8 +43,6 @@ import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.view.WindowManager; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.ViewTreeLifecycleOwner; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; @@ -188,24 +185,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test - public void attach_setsUpLifecycleOwner() { - mNotificationShadeWindowController.attach(); - - assertThat(ViewTreeLifecycleOwner.get(mNotificationShadeWindowView)).isNotNull(); - } - - @Test - public void attach_doesNotSetUpLifecycleOwnerIfAlreadySet() { - final LifecycleOwner previouslySet = mock(LifecycleOwner.class); - ViewTreeLifecycleOwner.set(mNotificationShadeWindowView, previouslySet); - - mNotificationShadeWindowController.attach(); - - assertThat(ViewTreeLifecycleOwner.get(mNotificationShadeWindowView)) - .isEqualTo(previouslySet); - } - - @Test public void setScrimsVisibility_earlyReturn() { clearInvocations(mWindowManager); mNotificationShadeWindowController.setScrimsVisibility(ScrimController.TRANSPARENT); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index c283cec5c15c..dfa38abc1ff8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -33,7 +33,6 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -1846,103 +1845,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test - public void stableOrderingDisregardedWithSectionChange() { - // GIVEN the first sectioner's packages can be changed from run-to-run - List<String> mutableSectionerPackages = new ArrayList<>(); - mutableSectionerPackages.add(PACKAGE_1); - mListBuilder.setSectioners(asList( - new PackageSectioner(mutableSectionerPackages, null), - new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null))); - mStabilityManager.setAllowEntryReordering(false); - - // WHEN the list is originally built with reordering disabled (and section changes allowed) - addNotif(0, PACKAGE_1).setRank(1); - addNotif(1, PACKAGE_1).setRank(5); - addNotif(2, PACKAGE_2).setRank(2); - addNotif(3, PACKAGE_2).setRank(3); - addNotif(4, PACKAGE_3).setRank(4); - dispatchBuild(); - - // VERIFY the order and that entry reordering has not been suppressed - verifyBuiltList( - notif(0), - notif(1), - notif(2), - notif(3), - notif(4) - ); - verify(mStabilityManager, never()).onEntryReorderSuppressed(); - - // WHEN the first section now claims PACKAGE_3 notifications - mutableSectionerPackages.add(PACKAGE_3); - dispatchBuild(); - - // VERIFY the re-sectioned notification is inserted at the top of the first section, because - // it's effectively "new" and "new" things are inserted at the top of their section. - verifyBuiltList( - notif(4), - notif(0), - notif(1), - notif(2), - notif(3) - ); - verify(mStabilityManager).onEntryReorderSuppressed(); - clearInvocations(mStabilityManager); - - // WHEN reordering is now allowed again - mStabilityManager.setAllowEntryReordering(true); - dispatchBuild(); - - // VERIFY that list order changes to put the re-sectioned notification in the middle where - // it is ranked. - verifyBuiltList( - notif(0), - notif(4), - notif(1), - notif(2), - notif(3) - ); - verify(mStabilityManager, never()).onEntryReorderSuppressed(); - } - - @Test - public void groupRevertingToSummaryRetainsStablePosition() { - // GIVEN a notification group is on screen - mStabilityManager.setAllowEntryReordering(false); - - // WHEN the list is originally built with reordering disabled (and section changes allowed) - addNotif(0, PACKAGE_1).setRank(2); - addNotif(1, PACKAGE_1).setRank(3); - addGroupSummary(2, PACKAGE_1, "group").setRank(4); - addGroupChild(3, PACKAGE_1, "group").setRank(5); - addGroupChild(4, PACKAGE_1, "group").setRank(6); - dispatchBuild(); - - verifyBuiltList( - notif(0), - notif(1), - group( - summary(2), - child(3), - child(4) - ) - ); - - // WHEN the notification summary rank increases and children removed - setNewRank(notif(2).entry, 1); - mEntrySet.remove(4); - mEntrySet.remove(3); - dispatchBuild(); - - // VERIFY the summary stays in the same location on rebuild - verifyBuiltList( - notif(0), - notif(1), - notif(2) - ); - } - - @Test public void testStableChildOrdering() { // WHEN the list is originally built with reordering disabled mStabilityManager.setAllowEntryReordering(false); @@ -2138,7 +2040,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { private void assertOrder(String visible, String active, String expected) { StringBuilder differenceSb = new StringBuilder(); - NotifSection section = new NotifSection(mock(NotifSectioner.class), 0); for (char c : active.toCharArray()) { if (visible.indexOf(c) < 0) differenceSb.append(c); } @@ -2147,7 +2048,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { for (int i = 0; i < visible.length(); i++) { addNotif(i, String.valueOf(visible.charAt(i))) .setRank(active.indexOf(visible.charAt(i))) - .setSection(section) .setStableIndex(i); } @@ -2155,7 +2055,6 @@ public class ShadeListBuilderTest extends SysuiTestCase { for (int i = 0; i < difference.length(); i++) { addNotif(i + visible.length(), String.valueOf(difference.charAt(i))) .setRank(active.indexOf(difference.charAt(i))) - .setSection(section) .setStableIndex(-1); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 59a9a3c74718..4fa45076a47c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.IActivityManager; import android.app.INotificationManager; import android.app.Notification; @@ -143,7 +144,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -252,6 +256,8 @@ public class BubblesTest extends SysuiTestCase { private TaskViewTransitions mTaskViewTransitions; @Mock private Optional<OneHandedController> mOneHandedOptional; + @Mock + private UserManager mUserManager; private TestableBubblePositioner mPositioner; @@ -314,6 +320,9 @@ public class BubblesTest extends SysuiTestCase { mPositioner.setMaxBubbles(5); mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor); + when(mUserManager.getProfiles(ActivityManager.getCurrentUser())).thenReturn( + Collections.singletonList(mock(UserInfo.class))); + TestableNotificationInterruptStateProviderImpl interruptionStateProvider = new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(), mock(PowerManager.class), @@ -339,7 +348,7 @@ public class BubblesTest extends SysuiTestCase { mStatusBarService, mWindowManager, mWindowManagerShellWrapper, - mock(UserManager.class), + mUserManager, mLauncherApps, mBubbleLogger, mTaskStackListener, @@ -1025,7 +1034,7 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull(); // Switch users - mBubbleController.onUserChanged(secondUserId); + switchUser(secondUserId); assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); // Give this user some bubbles @@ -1042,6 +1051,41 @@ public class BubblesTest extends SysuiTestCase { verify(mDataRepository, times(2)).loadBubbles(anyInt(), any()); } + @Test + public void testOnUserChanged_bubblesRestored() { + int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(); + int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier(); + // Mock current profile + when(mLockscreenUserManager.isCurrentProfile(firstUserId)).thenReturn(true); + when(mLockscreenUserManager.isCurrentProfile(secondUserId)).thenReturn(false); + + mBubbleController.updateBubble(mBubbleEntry); + assertThat(mBubbleController.hasBubbles()).isTrue(); + // We start with 1 bubble + assertThat(mBubbleData.getBubbles()).hasSize(1); + + // Switch to second user + switchUser(secondUserId); + + // Second user has no bubbles + assertThat(mBubbleController.hasBubbles()).isFalse(); + + // Send bubble update for first user, ensure it does not show up + mBubbleController.updateBubble(mBubbleEntry2); + assertThat(mBubbleController.hasBubbles()).isFalse(); + + // Start returning notif for first user again + when(mCommonNotifCollection.getAllNotifs()).thenReturn(Arrays.asList(mRow, mRow2)); + + // Switch back to first user + switchUser(firstUserId); + + // Check we now have two bubbles, one previous and one new that came in + assertThat(mBubbleController.hasBubbles()).isTrue(); + // Now there are 2 bubbles + assertThat(mBubbleData.getBubbles()).hasSize(2); + } + /** * Verifies we only load the overflow data once. */ @@ -1443,6 +1487,14 @@ public class BubblesTest extends SysuiTestCase { .build(); } + private void switchUser(int userId) { + when(mLockscreenUserManager.isCurrentProfile(anyInt())).thenAnswer( + (Answer<Boolean>) invocation -> invocation.<Integer>getArgument(0) == userId); + SparseArray<UserInfo> userInfos = new SparseArray<>(1); + userInfos.put(userId, mock(UserInfo.class)); + mBubbleController.onCurrentProfilesChanged(userInfos); + mBubbleController.onUserChanged(userId); + } /** * Asserts that the bubble stack is expanded and also validates the cached state is updated. diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java index 803177b6c010..3fa0ab69d67c 100644 --- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java +++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java @@ -16,8 +16,6 @@ package com.android.server.accessibility; -import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; - import android.accessibilityservice.AccessibilityService; import android.app.PendingIntent; import android.app.RemoteAction; @@ -34,6 +32,7 @@ import android.util.Slog; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.internal.R; @@ -392,8 +391,8 @@ public class SystemActionPerformer { private boolean takeScreenshot() { ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null) ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext); - screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN, - true, true, SCREENSHOT_ACCESSIBILITY_ACTIONS, + screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, + WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null); return true; } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index d34682df3413..13816142a633 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -1698,7 +1698,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt mDefaultPermissionCallback.onInstallPermissionGranted(); } - public void onPermissionRevoked(int uid, int userId, String reason) { + public void onPermissionRevoked(int uid, int userId, String reason, + boolean overrideKill, @Nullable String permissionName) { revokedPermissions.add(IntPair.of(uid, userId)); syncUpdatedUsers.add(userId); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index fe3f37dd4a27..99b34c78597c 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1661,9 +1661,6 @@ class ActivityStarter { && transitionController.getTransitionPlayer() != null) ? transitionController.createTransition(TRANSIT_OPEN) : null; RemoteTransition remoteTransition = r.takeRemoteTransition(); - if (newTransition != null && remoteTransition != null) { - newTransition.setRemoteTransition(remoteTransition); - } transitionController.collect(r); try { mService.deferWindowLayout(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 0260b78f49e4..c8fcee67a383 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5324,7 +5324,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public void setRunningRemoteTransitionDelegate(IApplicationThread caller) { + public void setRunningRemoteTransitionDelegate(IApplicationThread delegate) { + final TransitionController controller = getTransitionController(); + // A quick path without entering WM lock. + if (delegate != null && controller.mRemotePlayer.reportRunning(delegate)) { + // The delegate was known as running remote transition. + return; + } mAmInternal.enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, "setRunningRemoteTransition"); final int callingPid = Binder.getCallingPid(); @@ -5341,13 +5347,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { Slog.e(TAG, msg); throw new SecurityException(msg); } - final WindowProcessController wpc = getProcessController(caller); + final WindowProcessController wpc = getProcessController(delegate); if (wpc == null) { - Slog.w(TAG, "Unable to find process for application " + caller); + Slog.w(TAG, "setRunningRemoteTransition: no process for " + delegate); return; } - wpc.setRunningRemoteAnimation(true /* running */); - callingProc.addRemoteAnimationDelegate(wpc); + controller.mRemotePlayer.update(wpc, true /* running */, false /* predict */); } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 14e224111286..693f9c460ee6 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2748,8 +2748,6 @@ public class DisplayPolicy { public void takeScreenshot(int screenshotType, int source) { if (mScreenshotHelper != null) { mScreenshotHelper.takeScreenshot(screenshotType, - getStatusBar() != null && getStatusBar().isVisible(), - getNavigationBar() != null && getNavigationBar().isVisible(), source, mHandler, null /* completionConsumer */); } } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index a02be25bc8d2..5c20258538ee 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -45,6 +45,7 @@ import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import com.android.internal.annotations.GuardedBy; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; import com.android.server.LocalServices; @@ -77,9 +78,10 @@ class TransitionController { final TransitionMetricsReporter mTransitionMetricsReporter = new TransitionMetricsReporter(); final TransitionTracer mTransitionTracer; - private IApplicationThread mTransitionPlayerThread; + private WindowProcessController mTransitionPlayerProc; final ActivityTaskManagerService mAtm; final TaskSnapshotController mTaskSnapshotController; + final RemotePlayer mRemotePlayer; private final ArrayList<WindowManagerInternal.AppTransitionListener> mLegacyListeners = new ArrayList<>(); @@ -112,6 +114,7 @@ class TransitionController { TaskSnapshotController taskSnapshotController, TransitionTracer transitionTracer) { mAtm = atm; + mRemotePlayer = new RemotePlayer(atm); mStatusBar = LocalServices.getService(StatusBarManagerInternal.class); mTaskSnapshotController = taskSnapshotController; mTransitionTracer = transitionTracer; @@ -123,6 +126,8 @@ class TransitionController { } mPlayingTransitions.clear(); mTransitionPlayer = null; + mTransitionPlayerProc = null; + mRemotePlayer.clear(); mRunningLock.doNotifyLocked(); } }; @@ -169,7 +174,7 @@ class TransitionController { } void registerTransitionPlayer(@Nullable ITransitionPlayer player, - @Nullable IApplicationThread appThread) { + @Nullable WindowProcessController playerProc) { try { // Note: asBinder() can be null if player is same process (likely in a test). if (mTransitionPlayer != null) { @@ -182,7 +187,7 @@ class TransitionController { player.asBinder().linkToDeath(mTransitionPlayerDeath, 0); } mTransitionPlayer = player; - mTransitionPlayerThread = appThread; + mTransitionPlayerProc = playerProc; } catch (RemoteException e) { throw new RuntimeException("Unable to set transition player"); } @@ -421,6 +426,7 @@ class TransitionController { } mTransitionPlayer.requestStartTransition(transition, new TransitionRequestInfo( transition.mType, info, remoteTransition, displayChange)); + transition.setRemoteTransition(remoteTransition); } catch (RemoteException e) { Slog.e(TAG, "Error requesting transition", e); transition.start(); @@ -537,9 +543,7 @@ class TransitionController { } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record); mPlayingTransitions.remove(record); - if (mPlayingTransitions.isEmpty()) { - setAnimationRunning(false /* running */); - } + updateRunningRemoteAnimation(record, false /* isPlaying */); record.finishTransition(); mRunningLock.doNotifyLocked(); } @@ -549,21 +553,27 @@ class TransitionController { throw new IllegalStateException("Trying to move non-collecting transition to playing"); } mCollectingTransition = null; - if (mPlayingTransitions.isEmpty()) { - setAnimationRunning(true /* running */); - } mPlayingTransitions.add(transition); + updateRunningRemoteAnimation(transition, true /* isPlaying */); mTransitionTracer.logState(transition); } - private void setAnimationRunning(boolean running) { - if (mTransitionPlayerThread == null) return; - final WindowProcessController wpc = mAtm.getProcessController(mTransitionPlayerThread); - if (wpc == null) { - Slog.w(TAG, "Unable to find process for player thread=" + mTransitionPlayerThread); + /** Updates the process state of animation player. */ + private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) { + if (mTransitionPlayerProc == null) return; + if (isPlaying) { + mTransitionPlayerProc.setRunningRemoteAnimation(true); + } else if (mPlayingTransitions.isEmpty()) { + mTransitionPlayerProc.setRunningRemoteAnimation(false); + mRemotePlayer.clear(); return; } - wpc.setRunningRemoteAnimation(running); + final RemoteTransition remote = transition.getRemoteTransition(); + if (remote == null) return; + final IApplicationThread appThread = remote.getAppThread(); + final WindowProcessController delegate = mAtm.getProcessController(appThread); + if (delegate == null) return; + mRemotePlayer.update(delegate, isPlaying, true /* predict */); } void abort(Transition transition) { @@ -666,6 +676,95 @@ class TransitionController { proto.end(token); } + /** + * This manages the animating state of processes that are running remote animations for + * {@link #mTransitionPlayerProc}. + */ + static class RemotePlayer { + private static final long REPORT_RUNNING_GRACE_PERIOD_MS = 100; + @GuardedBy("itself") + private final ArrayMap<IBinder, DelegateProcess> mDelegateProcesses = new ArrayMap<>(); + private final ActivityTaskManagerService mAtm; + + private class DelegateProcess implements Runnable { + final WindowProcessController mProc; + /** Requires {@link RemotePlayer#reportRunning} to confirm it is really running. */ + boolean mNeedReport; + + DelegateProcess(WindowProcessController proc) { + mProc = proc; + } + + /** This runs when the remote player doesn't report running in time. */ + @Override + public void run() { + synchronized (mAtm.mGlobalLockWithoutBoost) { + update(mProc, false /* running */, false /* predict */); + } + } + } + + RemotePlayer(ActivityTaskManagerService atm) { + mAtm = atm; + } + + void update(@NonNull WindowProcessController delegate, boolean running, boolean predict) { + if (!running) { + synchronized (mDelegateProcesses) { + boolean removed = false; + for (int i = mDelegateProcesses.size() - 1; i >= 0; i--) { + if (mDelegateProcesses.valueAt(i).mProc == delegate) { + mDelegateProcesses.removeAt(i); + removed = true; + break; + } + } + if (!removed) return; + } + delegate.setRunningRemoteAnimation(false); + return; + } + if (delegate.isRunningRemoteTransition() || !delegate.hasThread()) return; + delegate.setRunningRemoteAnimation(true); + final DelegateProcess delegateProc = new DelegateProcess(delegate); + // If "predict" is true, that means the remote animation is set from + // ActivityOptions#makeRemoteAnimation(). But it is still up to shell side to decide + // whether to use the remote animation, so there is a timeout to cancel the prediction + // if the remote animation doesn't happen. + if (predict) { + delegateProc.mNeedReport = true; + mAtm.mH.postDelayed(delegateProc, REPORT_RUNNING_GRACE_PERIOD_MS); + } + synchronized (mDelegateProcesses) { + mDelegateProcesses.put(delegate.getThread().asBinder(), delegateProc); + } + } + + void clear() { + synchronized (mDelegateProcesses) { + for (int i = mDelegateProcesses.size() - 1; i >= 0; i--) { + mDelegateProcesses.valueAt(i).mProc.setRunningRemoteAnimation(false); + } + mDelegateProcesses.clear(); + } + } + + /** Returns {@code true} if the app is known to be running remote transition. */ + boolean reportRunning(@NonNull IApplicationThread appThread) { + final DelegateProcess delegate; + synchronized (mDelegateProcesses) { + delegate = mDelegateProcesses.get(appThread.asBinder()); + if (delegate != null && delegate.mNeedReport) { + // It was predicted to run remote transition. Now it is really requesting so + // remove the timeout of restoration. + delegate.mNeedReport = false; + mAtm.mH.removeCallbacks(delegate); + } + } + return delegate != null; + } + } + static class TransitionMetricsReporter extends ITransitionMetricsReporter.Stub { private final ArrayMap<IBinder, LongConsumer> mMetricConsumers = new ArrayMap<>(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0f5655c74f31..48740a3c2f50 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -55,7 +55,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.IApplicationThread; import android.app.WindowConfiguration; import android.content.ActivityNotFoundException; import android.content.Intent; @@ -1449,11 +1448,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub synchronized (mGlobalLock) { final WindowProcessController wpc = mService.getProcessController(callerPid, callerUid); - IApplicationThread appThread = null; - if (wpc != null) { - appThread = wpc.getThread(); - } - mTransitionController.registerTransitionPlayer(player, appThread); + mTransitionController.registerTransitionPlayer(player, wpc); } } finally { Binder.restoreCallingIdentity(ident); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 3ff912cca091..87b0c8b77904 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -77,7 +77,6 @@ import com.android.server.wm.ActivityTaskManagerService.HotPath; import java.io.IOException; import java.io.PrintWriter; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -227,10 +226,6 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio /** Whether our process is currently running a {@link IRemoteAnimationRunner} */ private boolean mRunningRemoteAnimation; - /** List of "chained" processes that are running remote animations for this process */ - private final ArrayList<WeakReference<WindowProcessController>> mRemoteAnimationDelegates = - new ArrayList<>(); - // The bits used for mActivityStateFlags. private static final int ACTIVITY_STATE_FLAG_IS_VISIBLE = 1 << 16; private static final int ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED = 1 << 17; @@ -1663,30 +1658,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio updateRunningRemoteOrRecentsAnimation(); } - /** - * Marks another process as a "delegate" animator. This means that process is doing some part - * of a remote animation on behalf of this process. - */ - void addRemoteAnimationDelegate(WindowProcessController delegate) { - if (!isRunningRemoteTransition()) { - throw new IllegalStateException("Can't add a delegate to a process which isn't itself" - + " running a remote animation"); - } - mRemoteAnimationDelegates.add(new WeakReference<>(delegate)); - } - void updateRunningRemoteOrRecentsAnimation() { - if (!isRunningRemoteTransition()) { - // Clean-up any delegates - for (int i = 0; i < mRemoteAnimationDelegates.size(); ++i) { - final WindowProcessController delegate = mRemoteAnimationDelegates.get(i).get(); - if (delegate == null) continue; - delegate.setRunningRemoteAnimation(false); - delegate.setRunningRecentsAnimation(false); - } - mRemoteAnimationDelegates.clear(); - } - // Posting on handler so WM lock isn't held when we call into AM. mAtm.mH.sendMessage(PooledLambda.obtainMessage( WindowProcessListener::setRunningRemoteAnimation, mListener, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java index 1d6ed038b86d..c15f6a9c3d66 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java @@ -17,6 +17,7 @@ package com.android.server.accessibility; import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; +import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; @@ -27,7 +28,6 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; @@ -301,8 +301,9 @@ public class SystemActionPerformerTest { mSystemActionPerformer.performSystemAction( AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT); verify(mMockScreenshotHelper).takeScreenshot( - eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(), - anyBoolean(), eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any()); + eq(TAKE_SCREENSHOT_FULLSCREEN), + eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), + any(Handler.class), any()); } // PendingIntent is a final class and cannot be mocked. So we are using this diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index c323e02ad149..a1ad07adbafb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -62,9 +62,11 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.view.SurfaceControl; import android.window.IDisplayAreaOrganizer; +import android.window.IRemoteTransition; import android.window.ITaskFragmentOrganizer; import android.window.ITaskOrganizer; import android.window.ITransitionPlayer; +import android.window.RemoteTransition; import android.window.TaskFragmentOrganizer; import android.window.TransitionInfo; @@ -413,6 +415,38 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testRunningRemoteTransition() { + final TestTransitionPlayer testPlayer = new TestTransitionPlayer( + mAtm.getTransitionController(), mAtm.mWindowOrganizerController); + final WindowProcessController playerProc = mSystemServicesTestRule.addProcess( + "pkg.player", "proc.player", 5000 /* pid */, 5000 /* uid */); + testPlayer.mController.registerTransitionPlayer(testPlayer, playerProc); + doReturn(mock(IBinder.class)).when(playerProc.getThread()).asBinder(); + final WindowProcessController delegateProc = mSystemServicesTestRule.addProcess( + "pkg.delegate", "proc.delegate", 6000 /* pid */, 6000 /* uid */); + doReturn(mock(IBinder.class)).when(delegateProc.getThread()).asBinder(); + final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final TransitionController controller = app.mTransitionController; + final Transition transition = controller.createTransition(TRANSIT_OPEN); + final RemoteTransition remoteTransition = new RemoteTransition( + mock(IRemoteTransition.class)); + remoteTransition.setAppThread(delegateProc.getThread()); + transition.collectExistenceChange(app.getTask()); + controller.requestStartTransition(transition, app.getTask(), remoteTransition, + null /* displayChange */); + testPlayer.startTransition(); + testPlayer.onTransactionReady(app.getSyncTransaction()); + assertTrue(playerProc.isRunningRemoteTransition()); + assertTrue(delegateProc.isRunningRemoteTransition()); + assertTrue(controller.mRemotePlayer.reportRunning(delegateProc.getThread())); + + testPlayer.finish(); + assertFalse(playerProc.isRunningRemoteTransition()); + assertFalse(delegateProc.isRunningRemoteTransition()); + assertFalse(controller.mRemotePlayer.reportRunning(delegateProc.getThread())); + } + + @Test public void testOpenActivityInTheSameTaskWithDisplayChange() { final ActivityRecord closing = createActivityRecord(mDisplayContent); closing.mVisibleRequested = true; @@ -861,7 +895,7 @@ public class TransitionTests extends WindowTestsBase { final TransitionController controller = new TransitionController(mAtm, snapshotController, mock(TransitionTracer.class)); final ITransitionPlayer player = new ITransitionPlayer.Default(); - controller.registerTransitionPlayer(player, null /* appThread */); + controller.registerTransitionPlayer(player, null /* playerProc */); final Transition openTransition = controller.createTransition(TRANSIT_OPEN); // Start out with task2 visible and set up a transition that closes task2 and opens task1 @@ -926,7 +960,7 @@ public class TransitionTests extends WindowTestsBase { final TransitionController controller = new TransitionController(mAtm, snapshotController, mock(TransitionTracer.class)); final ITransitionPlayer player = new ITransitionPlayer.Default(); - controller.registerTransitionPlayer(player, null /* appThread */); + controller.registerTransitionPlayer(player, null /* playerProc */); final Transition openTransition = controller.createTransition(TRANSIT_OPEN); // Start out with task2 visible and set up a transition that closes task2 and opens task1 @@ -990,7 +1024,7 @@ public class TransitionTests extends WindowTestsBase { final TransitionController controller = new TransitionController(mAtm, snapshotController, mock(TransitionTracer.class)); final ITransitionPlayer player = new ITransitionPlayer.Default(); - controller.registerTransitionPlayer(player, null /* appThread */); + controller.registerTransitionPlayer(player, null /* playerProc */); final Transition openTransition = controller.createTransition(TRANSIT_OPEN); // Start out with task2 visible and set up a transition that closes task2 and opens task1 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 0cbf1b2c7cc8..ee5f36412df2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -830,7 +830,7 @@ class WindowTestsBase extends SystemServiceTestsBase { TestTransitionPlayer registerTestTransitionPlayer() { final TestTransitionPlayer testPlayer = new TestTransitionPlayer( mAtm.getTransitionController(), mAtm.mWindowOrganizerController); - testPlayer.mController.registerTransitionPlayer(testPlayer, null /* appThread */); + testPlayer.mController.registerTransitionPlayer(testPlayer, null /* playerProc */); return testPlayer; } |