diff options
57 files changed, 6300 insertions, 366 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 19f68eb0c787..26c64bd7adbc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10249,23 +10249,23 @@ package android.companion.virtual { public final class VirtualDevice implements android.os.Parcelable { method public int describeContents(); method public int getDeviceId(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @NonNull public int[] getDisplayIds(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public CharSequence getDisplayName(); + method @NonNull public int[] getDisplayIds(); + method @Nullable public CharSequence getDisplayName(); method @Nullable public String getName(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public String getPersistentDeviceId(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomSensorSupport(); + method @Nullable public String getPersistentDeviceId(); + method public boolean hasCustomSensorSupport(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.VirtualDevice> CREATOR; } public final class VirtualDeviceManager { - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public android.companion.virtual.VirtualDevice getVirtualDevice(int); + method @Nullable public android.companion.virtual.VirtualDevice getVirtualDevice(int); method @NonNull public java.util.List<android.companion.virtual.VirtualDevice> getVirtualDevices(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public void registerVirtualDeviceListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.VirtualDeviceListener); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public void unregisterVirtualDeviceListener(@NonNull android.companion.virtual.VirtualDeviceManager.VirtualDeviceListener); + method public void registerVirtualDeviceListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.VirtualDeviceListener); + method public void unregisterVirtualDeviceListener(@NonNull android.companion.virtual.VirtualDeviceManager.VirtualDeviceListener); } - @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public static interface VirtualDeviceManager.VirtualDeviceListener { + public static interface VirtualDeviceManager.VirtualDeviceListener { method public default void onVirtualDeviceClosed(int); method public default void onVirtualDeviceCreated(int); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 6dedfa4cf535..f44448a8c311 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3393,8 +3393,8 @@ package android.companion.virtual { } public final class VirtualDevice implements android.os.Parcelable { - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomAudioInputSupport(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomCameraSupport(); + method public boolean hasCustomAudioInputSupport(); + method public boolean hasCustomCameraSupport(); } public final class VirtualDeviceManager { @@ -3446,7 +3446,7 @@ package android.companion.virtual { method @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig); method @Deprecated @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method public int getDeviceId(); - method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public String getPersistentDeviceId(); + method @Nullable public String getPersistentDeviceId(); method @NonNull public java.util.List<android.companion.virtual.sensor.VirtualSensor> getVirtualSensorList(); method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") public void goToSleep(); method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); @@ -5319,7 +5319,7 @@ package android.hardware.display { method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfiguration(android.hardware.display.BrightnessConfiguration); method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfigurationForDisplay(@NonNull android.hardware.display.BrightnessConfiguration, @NonNull String); method @Deprecated @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_SATURATION) public void setSaturationLevel(float); - field @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 128; // 0x80 + field public static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 128; // 0x80 field public static final int VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED = 65536; // 0x10000 field public static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1024; // 0x400 } diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java index b9e9afea8893..8ef4224975bf 100644 --- a/core/java/android/companion/virtual/VirtualDevice.java +++ b/core/java/android/companion/virtual/VirtualDevice.java @@ -20,11 +20,9 @@ import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; @@ -34,8 +32,9 @@ import android.os.RemoteException; * Details of a particular virtual device. * * <p>Read-only device representation exposing the properties of an existing virtual device. + * + * @see VirtualDeviceManager#registerVirtualDeviceListener */ -// TODO(b/310912420): Link to VirtualDeviceManager#registerVirtualDeviceListener from the docs public final class VirtualDevice implements Parcelable { private final @NonNull IVirtualDevice mVirtualDevice; @@ -93,8 +92,8 @@ public final class VirtualDevice implements Parcelable { * per device. * * @see Context#createDeviceContext + * @see #getPersistentDeviceId() */ - // TODO(b/310912420): Link to #getPersistentDeviceId from the docs public int getDeviceId() { return mId; } @@ -111,7 +110,6 @@ public final class VirtualDevice implements Parcelable { * <p class="note">This identifier may not be unique across virtual devices, in case there are * more than one virtual devices corresponding to the same physical device. */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public @Nullable String getPersistentDeviceId() { return mPersistentId; } @@ -127,7 +125,6 @@ public final class VirtualDevice implements Parcelable { * Returns the human-readable name of the virtual device, if defined, which is suitable to be * shown in UI. */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public @Nullable CharSequence getDisplayName() { return mDisplayName; } @@ -138,7 +135,6 @@ public final class VirtualDevice implements Parcelable { * <p>The actual {@link android.view.Display} objects can be obtained by passing the returned * IDs to {@link android.hardware.display.DisplayManager#getDisplay(int)}.</p> */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public @NonNull int[] getDisplayIds() { try { return mVirtualDevice.getDisplayIds(); @@ -157,7 +153,6 @@ public final class VirtualDevice implements Parcelable { * @see Context#getDeviceId() * @see Context#createDeviceContext(int) */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public boolean hasCustomSensorSupport() { try { return mVirtualDevice.getDevicePolicy(POLICY_TYPE_SENSORS) == DEVICE_POLICY_CUSTOM; @@ -172,7 +167,6 @@ public final class VirtualDevice implements Parcelable { * @hide */ @SystemApi - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public boolean hasCustomAudioInputSupport() { try { return mVirtualDevice.hasCustomAudioInputSupport(); @@ -194,7 +188,6 @@ public final class VirtualDevice implements Parcelable { * @hide */ @SystemApi - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public boolean hasCustomCameraSupport() { try { return mVirtualDevice.getDevicePolicy(POLICY_TYPE_CAMERA) == DEVICE_POLICY_CUSTOM; diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 91ea673ab6f9..99794d7f49fe 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -223,10 +223,9 @@ public final class VirtualDeviceManager { * existing virtual devices.</p> * * <p>Note that if a virtual device is closed and becomes invalid, the returned objects will - * not be updated and may contain stale values.</p> + * not be updated and may contain stale values. Use a {@link VirtualDeviceListener} for real + * time updates of the availability of virtual devices.</p> */ - // TODO(b/310912420): Add "Use a VirtualDeviceListener for real time updates of the - // availability of virtual devices." in the note paragraph above with a link annotation. @NonNull public List<android.companion.virtual.VirtualDevice> getVirtualDevices() { if (mService == null) { @@ -253,7 +252,6 @@ public final class VirtualDeviceManager { * @return the virtual device with the requested ID, or {@code null} if no such device exists or * it has already been closed. */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) @Nullable public android.companion.virtual.VirtualDevice getVirtualDevice(int deviceId) { if (mService == null) { @@ -278,7 +276,6 @@ public final class VirtualDeviceManager { * @param listener The listener to add. * @see #unregisterVirtualDeviceListener */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public void registerVirtualDeviceListener( @NonNull @CallbackExecutor Executor executor, @NonNull VirtualDeviceListener listener) { @@ -306,7 +303,6 @@ public final class VirtualDeviceManager { * @param listener The listener to unregister. * @see #registerVirtualDeviceListener */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public void unregisterVirtualDeviceListener(@NonNull VirtualDeviceListener listener) { if (mService == null) { Log.w(TAG, "Failed to unregister listener; no virtual device manager service."); @@ -389,9 +385,9 @@ public final class VirtualDeviceManager { * @return the display name associated with the given persistent device ID, or {@code null} if * the persistent ID is invalid or does not correspond to a virtual device. * + * @see VirtualDevice#getPersistentDeviceId() * @hide */ - // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId() @SystemApi @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String persistentDeviceId) { @@ -411,9 +407,9 @@ public final class VirtualDeviceManager { * Returns all current persistent device IDs, including the ones for which no virtual device * exists, as long as one may have existed or can be created. * + * @see VirtualDevice#getPersistentDeviceId() * @hide */ - // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId() @SystemApi @NonNull public Set<String> getAllPersistentDeviceIds() { @@ -588,7 +584,6 @@ public final class VirtualDeviceManager { /** * Returns the persistent ID of this virtual device. */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public @Nullable String getPersistentDeviceId() { return mVirtualDeviceInternal.getPersistentDeviceId(); } @@ -1339,7 +1334,6 @@ public final class VirtualDeviceManager { * * @see #registerVirtualDeviceListener */ - @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public interface VirtualDeviceListener { /** * Called whenever a new virtual device has been added to the system. diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 6716598f9e4c..0590a06f3f82 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -397,7 +397,6 @@ public final class DisplayManager { * @see #createVirtualDisplay * @hide */ - @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS) @SystemApi public static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 343d7527ea98..518820430419 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -2045,7 +2045,7 @@ public final class PermissionManager { if (deviceId == Context.DEVICE_ID_DEFAULT) { persistentDeviceId = VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT; - } else if (android.companion.virtual.flags.Flags.vdmPublicApis()) { + } else { VirtualDeviceManager virtualDeviceManager = mContext.getSystemService( VirtualDeviceManager.class); if (virtualDeviceManager != null) { @@ -2059,9 +2059,6 @@ public final class PermissionManager { Slog.e(LOG_TAG, "Cannot find persistent device Id for " + deviceId); } } - } else { - Slog.e(LOG_TAG, "vdmPublicApis flag is not enabled when device Id " + deviceId - + "is not default."); } return persistentDeviceId; } diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 7be6950fb613..ca6ad6fae46e 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2504,21 +2504,6 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_setProgressSegments() { - final List<Notification.ProgressStyle.Segment> segments = List.of( - new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE), - new Notification.ProgressStyle.Segment(50).setColor(Color.RED), - new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE) - ); - - final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); - progressStyle1.setProgressSegments(segments); - - assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments); - } - - @Test - @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_addProgressPoint_dropsNegativePoints() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2547,21 +2532,6 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_setProgressPoints() { - final List<Notification.ProgressStyle.Point> points = List.of( - new Notification.ProgressStyle.Point(0).setColor(Color.WHITE), - new Notification.ProgressStyle.Point(50).setColor(Color.RED), - new Notification.ProgressStyle.Point(100).setColor(Color.BLUE) - ); - - final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); - progressStyle1.setProgressPoints(points); - - assertThat(progressStyle1.getProgressPoints()).isEqualTo(points); - } - - @Test - @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2703,58 +2673,11 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_setProgressIndeterminate() { - final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); - progressStyle1.setProgressIndeterminate(true); - assertThat(progressStyle1.isProgressIndeterminate()).isTrue(); - } - - @Test - @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_styledByProgress_defaultValueTrue() { final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); assertThat(progressStyle1.isStyledByProgress()).isTrue(); } - - @Test - @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_setStyledByProgress() { - final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); - progressStyle1.setStyledByProgress(false); - assertThat(progressStyle1.isStyledByProgress()).isFalse(); - } - - @Test - @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_point() { - final int id = 1; - final int position = 10; - final int color = Color.RED; - - final Notification.ProgressStyle.Point point = - new Notification.ProgressStyle.Point(position).setId(id).setColor(color); - - assertEquals(id, point.getId()); - assertEquals(position, point.getPosition()); - assertEquals(color, point.getColor()); - } - - @Test - @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_segment() { - final int id = 1; - final int length = 100; - final int color = Color.RED; - - final Notification.ProgressStyle.Segment segment = - new Notification.ProgressStyle.Segment(length).setId(id).setColor(color); - - assertEquals(id, segment.getId()); - assertEquals(length, segment.getLength()); - assertEquals(color, segment.getColor()); - } - private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index ac510f89b905..e8add56619c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -946,7 +946,8 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -962,7 +963,7 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader)); + taskResourceLoader, recentsTransitionHandler)); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl index 32c79a2d02de..8cdb8c4512a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -17,9 +17,10 @@ package com.android.wm.shell.recents; import android.graphics.Rect; +import android.os.Bundle; import android.view.RemoteAnimationTarget; import android.window.TaskSnapshot; -import android.os.Bundle; +import android.window.TransitionInfo; import com.android.wm.shell.recents.IRecentsAnimationController; @@ -57,7 +58,8 @@ oneway interface IRecentsAnimationRunner { */ void onAnimationStart(in IRecentsAnimationController controller, in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, - in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2; + in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras, + in TransitionInfo info) = 2; /** * Called when the task of an activity that has been started while the recents animation diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index db582aa30f6a..aeccd86e122c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -587,7 +587,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), new RemoteAnimationTarget[0], - new Rect(0, 0, 0, 0), new Rect(), new Bundle()); + new Rect(0, 0, 0, 0), new Rect(), new Bundle(), + null); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING); } @@ -818,7 +819,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]), - new Rect(0, 0, 0, 0), new Rect(), b); + new Rect(0, 0, 0, 0), new Rect(), b, info); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 67dae283345a..055bc8f5f092 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -126,6 +126,8 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppToWebEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; @@ -157,8 +159,10 @@ import kotlinx.coroutines.MainCoroutineDispatcher; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; /** @@ -247,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final DesktopModeEventLogger mDesktopModeEventLogger; private final DesktopModeUiEventLogger mDesktopModeUiEventLogger; private final WindowDecorTaskResourceLoader mTaskResourceLoader; + private final RecentsTransitionHandler mRecentsTransitionHandler; public DesktopModeWindowDecorViewModel( Context context, @@ -282,7 +287,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader) { + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler) { this( context, shellExecutor, @@ -323,7 +329,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader); + taskResourceLoader, + recentsTransitionHandler); } @VisibleForTesting @@ -367,7 +374,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader) { + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -436,6 +444,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeEventLogger = desktopModeEventLogger; mDesktopModeUiEventLogger = desktopModeUiEventLogger; mTaskResourceLoader = taskResourceLoader; + mRecentsTransitionHandler = recentsTransitionHandler; shellInit.addInitCallback(this::onInit, this); } @@ -450,6 +459,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, new DesktopModeOnTaskResizeAnimationListener()); mDesktopTasksController.setOnTaskRepositionAnimationListener( new DesktopModeOnTaskRepositionAnimationListener()); + if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + mRecentsTransitionHandler.addTransitionStateListener( + new DesktopModeRecentsTransitionStateListener()); + } mDisplayController.addDisplayChangingController(mOnDisplayChangingListener); try { mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, @@ -1859,6 +1872,38 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } } + private class DesktopModeRecentsTransitionStateListener + implements RecentsTransitionStateListener { + final Set<Integer> mAnimatingTaskIds = new HashSet<>(); + + @Override + public void onTransitionStateChanged(int state) { + switch (state) { + case RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED: + for (int n = 0; n < mWindowDecorByTaskId.size(); n++) { + int taskId = mWindowDecorByTaskId.keyAt(n); + mAnimatingTaskIds.add(taskId); + setIsRecentsTransitionRunningForTask(taskId, true); + } + return; + case RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING: + // No Recents transition running - clean up window decorations + for (int taskId : mAnimatingTaskIds) { + setIsRecentsTransitionRunningForTask(taskId, false); + } + mAnimatingTaskIds.clear(); + return; + default: + } + } + + private void setIsRecentsTransitionRunningForTask(int taskId, boolean isRecentsRunning) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) return; + decoration.setIsRecentsTransitionRunning(isRecentsRunning); + } + } + private class DragEventListenerImpl implements DragPositioningCallbackUtility.DragEventListener { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4ac89546c9c7..39a989ce7c7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -204,6 +204,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopUserRepositories mDesktopUserRepositories; + private boolean mIsRecentsTransitionRunning = false; private Runnable mLoadAppInfoRunnable; private Runnable mSetAppInfoRunnable; @@ -498,7 +499,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, - displayExclusionRegion); + displayExclusionRegion, mIsRecentsTransitionRunning); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -869,7 +870,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean inFullImmersiveMode, @NonNull InsetsState displayInsetsState, boolean hasGlobalFocus, - @NonNull Region displayExclusionRegion) { + @NonNull Region displayExclusionRegion, + boolean shouldIgnoreCornerRadius) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -1006,13 +1008,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mWindowDecorConfig = windowDecorConfig; if (DesktopModeStatus.useRoundedCorners()) { - relayoutParams.mCornerRadius = taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - ? loadDimensionPixelSize(context.getResources(), - R.dimen.desktop_windowing_freeform_rounded_corner_radius) - : INVALID_CORNER_RADIUS; + relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS : + getCornerRadius(context, relayoutParams.mLayoutResId); } } + private static int getCornerRadius(@NonNull Context context, int layoutResId) { + if (layoutResId == R.layout.desktop_mode_app_header) { + return loadDimensionPixelSize(context.getResources(), + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + return INVALID_CORNER_RADIUS; + } + /** * If task has focused window decor, return the caption id of the fullscreen caption size * resource. Otherwise, return ID_NULL and caption width be set to task width. @@ -1740,6 +1748,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** + * Declares whether a Recents transition is currently active. + * + * <p> When a Recents transition is active we allow that transition to take ownership of the + * corner radius of its task surfaces, so each window decoration should stop updating the corner + * radius of its task surface during that time. + */ + void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) { + mIsRecentsTransitionRunning = isRecentsTransitionRunning; + } + + /** * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeButtonHoverExit() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 5d1bedb85b5e..fa7183ad0fd8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -967,4 +967,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags); } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index f549b0563827..2b986d184c20 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -1800,8 +1800,35 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) + fun moveToNextDisplay_wallpaperOnSystemUser_reorderWallpaperToBack() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Add a task and a wallpaper + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val wallpaperChange = + hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() } + assertNotNull(wallpaperChange) + assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + } + } + + @Test @EnableFlags(FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) - fun moveToNextDisplay_removeWallpaper() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + fun moveToNextDisplay_wallpaperNotOnSystemUser_removeWallpaper() { // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index 894d238b7e15..ab43119b14c0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -169,7 +169,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { final IResultReceiver finishCallback = mock(IResultReceiver.class); final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner); - verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any()); // Finish and verify no transition remains and that the provided finish callback is called mRecentsTransitionHandler.findController(transition).finish(true /* toHome */, @@ -184,7 +184,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner); - verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any()); mRecentsTransitionHandler.findController(transition).cancel("test"); mMainExecutor.flushAll(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ffe8e7135513..79e9b9c8cd77 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -59,11 +59,12 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.window.flags.Flags import com.android.wm.shell.R -import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.DesktopImmersiveController import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition +import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction +import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.splitscreen.SplitScreenController @@ -539,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onLeftSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), + .snapToHalfScreen( + eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), eq(decor), @@ -616,11 +618,12 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onRightSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), + .snapToHalfScreen( + eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), eq(decor), - ) + ) } @Test @@ -1223,6 +1226,49 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest verify(task2, never()).onExclusionRegionChanged(newRegion) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_requestedState_setsTransitionRunning() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + + verify(decoration).setIsRecentsTransitionRunning(true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_nonRunningState_setsTransitionNotRunning() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING) + + verify(decoration).setIsRecentsTransitionRunning(false) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_requestedAndAnimating_setsTransitionRunningOnce() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING) + + verify(decoration, times(1)).setIsRecentsTransitionRunning(true) + } + private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 40015eece15c..b44af4733fd2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -40,6 +40,7 @@ import android.view.SurfaceControl import android.view.WindowInsets.Type.statusBars import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -65,6 +66,8 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.desktopmode.education.AppToWebEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter +import com.android.wm.shell.recents.RecentsTransitionHandler +import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -151,6 +154,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>() protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>() protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>() + protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>() protected val motionEvent = mock<MotionEvent>() val displayLayout = mock<DisplayLayout>() protected lateinit var spyContext: TestableContext @@ -164,6 +168,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected lateinit var mockitoSession: StaticMockitoSession protected lateinit var shellInit: ShellInit internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener + protected lateinit var desktopModeRecentsTransitionStateListener: RecentsTransitionStateListener protected lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener @@ -220,7 +225,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockFocusTransitionObserver, desktopModeEventLogger, mock<DesktopModeUiEventLogger>(), - mock<WindowDecorTaskResourceLoader>() + mock<WindowDecorTaskResourceLoader>(), + mockRecentsTransitionHandler, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -257,6 +263,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { verify(displayInsetsController) .addGlobalInsetsChangedListener(insetsChangedCaptor.capture()) desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue + val recentsTransitionStateListenerCaptor = argumentCaptor<RecentsTransitionStateListener>() + if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + verify(mockRecentsTransitionHandler) + .addTransitionStateListener(recentsTransitionStateListenerCaptor.capture()) + desktopModeRecentsTransitionStateListener = + recentsTransitionStateListenerCaptor.firstValue + } val keyguardChangedCaptor = argumentCaptor<DesktopModeKeyguardChangeListener>() verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 6b02aeffd42a..9ea5fd6e1abe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -169,6 +169,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false; private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false; private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; + private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -396,6 +397,31 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + public void updateRelayoutParams_shouldIgnoreCornerRadius_roundedCornersNotSet() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + fillRoundedCornersResources(/* fillValue= */ 30); + RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + mMockSplitScreenController, + DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, + DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, + DEFAULT_IS_STATUSBAR_VISIBLE, + DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, + DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + new InsetsState(), + DEFAULT_HAS_GLOBAL_FOCUS, + mExclusionRegion, + /* shouldIgnoreCornerRadius= */ true); + + assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) public void updateRelayoutParams_appHeader_usesTaskDensity() { final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() @@ -634,7 +660,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, insetsState, DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -659,7 +686,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -683,7 +711,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -707,7 +736,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -730,7 +760,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -753,7 +784,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -777,7 +809,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -793,7 +826,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -817,7 +851,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1480,7 +1515,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); } private DesktopModeWindowDecoration createWindowDecoration( diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java b/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java new file mode 100644 index 000000000000..fdb0fc538fdf --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.watchdog; + +import static android.os.Parcelable.Creator; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.crashrecovery.flags.Flags; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * A service to provide packages supporting explicit health checks and route checks to these + * packages on behalf of the package watchdog. + * + * <p>To extend this class, you must declare the service in your manifest file with the + * {@link android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission, + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. In adddition, + * your implementation must live in + * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}. + * For example:</p> + * <pre> + * <service android:name=".FooExplicitHealthCheckService" + * android:exported="true" + * android:priority="100" + * android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"> + * <intent-filter> + * <action android:name="android.service.watchdog.ExplicitHealthCheckService" /> + * </intent-filter> + * </service> + * </pre> + * @hide + */ +@SystemApi +public abstract class ExplicitHealthCheckService extends Service { + + private static final String TAG = "ExplicitHealthCheckService"; + + /** + * {@link Bundle} key for a {@link List} of {@link PackageConfig} value. + * + * {@hide} + */ + public static final String EXTRA_SUPPORTED_PACKAGES = + "android.service.watchdog.extra.supported_packages"; + + /** + * {@link Bundle} key for a {@link List} of {@link String} value. + * + * {@hide} + */ + public static final String EXTRA_REQUESTED_PACKAGES = + "android.service.watchdog.extra.requested_packages"; + + /** + * {@link Bundle} key for a {@link String} value. + */ + @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) + public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE = + "android.service.watchdog.extra.HEALTH_CHECK_PASSED_PACKAGE"; + + /** + * The Intent action that a service must respond to. Add it to the intent filter of the service + * in its manifest. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.watchdog.ExplicitHealthCheckService"; + + /** + * The permission that a service must require to ensure that only Android system can bind to it. + * If this permission is not enforced in the AndroidManifest of the service, the system will + * skip that service. + */ + public static final String BIND_PERMISSION = + "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"; + + private final ExplicitHealthCheckServiceWrapper mWrapper = + new ExplicitHealthCheckServiceWrapper(); + + /** + * Called when the system requests an explicit health check for {@code packageName}. + * + * <p> When {@code packageName} passes the check, implementors should call + * {@link #notifyHealthCheckPassed} to inform the system. + * + * <p> It could take many hours before a {@code packageName} passes a check and implementors + * should never drop requests unless {@link onCancel} is called or the service dies. + * + * <p> Requests should not be queued and additional calls while expecting a result for + * {@code packageName} should have no effect. + */ + public abstract void onRequestHealthCheck(@NonNull String packageName); + + /** + * Called when the system cancels the explicit health check request for {@code packageName}. + * Should do nothing if there are is no active request for {@code packageName}. + */ + public abstract void onCancelHealthCheck(@NonNull String packageName); + + /** + * Called when the system requests for all the packages supporting explicit health checks. The + * system may request an explicit health check for any of these packages with + * {@link #onRequestHealthCheck}. + * + * @return all packages supporting explicit health checks + */ + @NonNull public abstract List<PackageConfig> onGetSupportedPackages(); + + /** + * Called when the system requests for all the packages that it has currently requested + * an explicit health check for. + * + * @return all packages expecting an explicit health check result + */ + @NonNull public abstract List<String> onGetRequestedPackages(); + + private final Handler mHandler = Handler.createAsync(Looper.getMainLooper()); + @Nullable private Consumer<Bundle> mHealthCheckResultCallback; + @Nullable private Executor mCallbackExecutor; + + @Override + @NonNull + public final IBinder onBind(@NonNull Intent intent) { + return mWrapper; + } + + /** + * Sets a callback to be invoked when an explicit health check passes for a package. + * <p> + * The callback will receive a {@link Bundle} containing the package name that passed the + * health check, identified by the key {@link #EXTRA_HEALTH_CHECK_PASSED_PACKAGE}. + * <p> + * <b>Note:</b> This API is primarily intended for testing purposes. Calling this outside of a + * test environment will override the default callback mechanism used to notify the system + * about health check results. Use with caution in production code. + * + * @param executor The executor on which the callback should be invoked. If {@code null}, the + * callback will be executed on the main thread. + * @param callback A callback that receives a {@link Bundle} containing the package name that + * passed the health check. + */ + @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) + public final void setHealthCheckPassedCallback(@CallbackExecutor @Nullable Executor executor, + @Nullable Consumer<Bundle> callback) { + mCallbackExecutor = executor; + mHealthCheckResultCallback = callback; + } + + private void executeCallback(@NonNull String packageName) { + if (mHealthCheckResultCallback != null) { + Objects.requireNonNull(packageName, + "Package passing explicit health check must be non-null"); + Bundle bundle = new Bundle(); + bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName); + mHealthCheckResultCallback.accept(bundle); + } else { + Log.wtf(TAG, "System missed explicit health check result for " + packageName); + } + } + + /** + * Implementors should call this to notify the system when explicit health check passes + * for {@code packageName}; + */ + public final void notifyHealthCheckPassed(@NonNull String packageName) { + if (mCallbackExecutor != null) { + mCallbackExecutor.execute(() -> executeCallback(packageName)); + } else { + mHandler.post(() -> executeCallback(packageName)); + } + } + + /** + * A PackageConfig contains a package supporting explicit health checks and the + * timeout in {@link System#uptimeMillis} across reboots after which health + * check requests from clients are failed. + * + * @hide + */ + @SystemApi + public static final class PackageConfig implements Parcelable { + private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1); + + private final String mPackageName; + private final long mHealthCheckTimeoutMillis; + + /** + * Creates a new instance. + * + * @param packageName the package name + * @param durationMillis the duration in milliseconds, must be greater than or + * equal to 0. If it is 0, it will use a system default value. + */ + public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) { + mPackageName = Preconditions.checkNotNull(packageName); + if (healthCheckTimeoutMillis == 0) { + mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS; + } else { + mHealthCheckTimeoutMillis = Preconditions.checkArgumentNonnegative( + healthCheckTimeoutMillis); + } + } + + private PackageConfig(Parcel parcel) { + mPackageName = parcel.readString(); + mHealthCheckTimeoutMillis = parcel.readLong(); + } + + /** + * Gets the package name. + * + * @return the package name + */ + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * Gets the timeout in milliseconds to evaluate an explicit health check result after a + * request. + * + * @return the duration in {@link System#uptimeMillis} across reboots + */ + public long getHealthCheckTimeoutMillis() { + return mHealthCheckTimeoutMillis; + } + + @NonNull + @Override + public String toString() { + return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}"; + } + + @Override + public boolean equals(@Nullable Object other) { + if (other == this) { + return true; + } + if (!(other instanceof PackageConfig)) { + return false; + } + + PackageConfig otherInfo = (PackageConfig) other; + return Objects.equals(otherInfo.getHealthCheckTimeoutMillis(), + mHealthCheckTimeoutMillis) + && Objects.equals(otherInfo.getPackageName(), mPackageName); + } + + @Override + public int hashCode() { + return Objects.hash(mPackageName, mHealthCheckTimeoutMillis); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) { + parcel.writeString(mPackageName); + parcel.writeLong(mHealthCheckTimeoutMillis); + } + + public static final @NonNull Creator<PackageConfig> CREATOR = new Creator<PackageConfig>() { + @Override + public PackageConfig createFromParcel(Parcel source) { + return new PackageConfig(source); + } + + @Override + public PackageConfig[] newArray(int size) { + return new PackageConfig[size]; + } + }; + } + + + private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub { + @Override + public void setCallback(RemoteCallback callback) throws RemoteException { + mHandler.post(() -> mHealthCheckResultCallback = callback::sendResult); + } + + @Override + public void request(String packageName) throws RemoteException { + mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName)); + } + + @Override + public void cancel(String packageName) throws RemoteException { + mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName)); + } + + @Override + public void getSupportedPackages(RemoteCallback callback) throws RemoteException { + mHandler.post(() -> { + List<PackageConfig> packages = + ExplicitHealthCheckService.this.onGetSupportedPackages(); + Objects.requireNonNull(packages, "Supported package list must be non-null"); + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages)); + callback.sendResult(bundle); + }); + } + + @Override + public void getRequestedPackages(RemoteCallback callback) throws RemoteException { + mHandler.post(() -> { + List<String> packages = + ExplicitHealthCheckService.this.onGetRequestedPackages(); + Objects.requireNonNull(packages, "Requested package list must be non-null"); + Bundle bundle = new Bundle(); + bundle.putStringArrayList(EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages)); + callback.sendResult(bundle); + }); + } + } +} diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl b/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl new file mode 100644 index 000000000000..90965092ac2b --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.watchdog; + +import android.os.RemoteCallback; + +/** + * @hide + */ +@PermissionManuallyEnforced +oneway interface IExplicitHealthCheckService +{ + void setCallback(in @nullable RemoteCallback callback); + void request(String packageName); + void cancel(String packageName); + void getSupportedPackages(in RemoteCallback callback); + void getRequestedPackages(in RemoteCallback callback); +} diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS b/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS new file mode 100644 index 000000000000..1c045e10c0ec --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS @@ -0,0 +1,3 @@ +narayan@google.com +nandana@google.com +olilan@google.com diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl b/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl new file mode 100644 index 000000000000..013158676f79 --- /dev/null +++ b/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.watchdog; + +/** + * @hide + */ +parcelable PackageConfig; diff --git a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java new file mode 100644 index 000000000000..da9a13961f79 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES; +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; + +import android.Manifest; +import android.annotation.MainThread; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.watchdog.ExplicitHealthCheckService; +import android.service.watchdog.IExplicitHealthCheckService; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +// TODO(b/120598832): Add tests +/** + * Controls the connections with {@link ExplicitHealthCheckService}. + */ +class ExplicitHealthCheckController { + private static final String TAG = "ExplicitHealthCheckController"; + private final Object mLock = new Object(); + private final Context mContext; + + // Called everytime a package passes the health check, so the watchdog is notified of the + // passing check. In practice, should never be null after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. + @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer; + // Called everytime after a successful #syncRequest call, so the watchdog can receive packages + // supporting health checks and update its internal state. In practice, should never be null + // after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer. + @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer; + // Called everytime we need to notify the watchdog to sync requests between itself and the + // health check service. In practice, should never be null after it has been #setEnabled. + // To prevent deadlocks between the controller and watchdog threads, we have + // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class. + // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable. + @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable; + // Actual binder object to the explicit health check service. + @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService; + // Connection to the explicit health check service, necessary to unbind. + // We should only try to bind if mConnection is null, non-null indicates we + // are connected or at least connecting. + @GuardedBy("mLock") @Nullable private ServiceConnection mConnection; + // Bind state of the explicit health check service. + @GuardedBy("mLock") private boolean mEnabled; + + ExplicitHealthCheckController(Context context) { + mContext = context; + } + + /** Enables or disables explicit health checks. */ + public void setEnabled(boolean enabled) { + synchronized (mLock) { + Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled.")); + mEnabled = enabled; + } + } + + /** + * Sets callbacks to listen to important events from the controller. + * + * <p> Should be called once at initialization before any other calls to the controller to + * ensure a happens-before relationship of the set parameters and visibility on other threads. + */ + public void setCallbacks(Consumer<String> passedConsumer, + Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { + synchronized (mLock) { + if (mPassedConsumer != null || mSupportedConsumer != null + || mNotifySyncRunnable != null) { + Slog.wtf(TAG, "Resetting health check controller callbacks"); + } + + mPassedConsumer = Objects.requireNonNull(passedConsumer); + mSupportedConsumer = Objects.requireNonNull(supportedConsumer); + mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable); + } + } + + /** + * Calls the health check service to request or cancel packages based on + * {@code newRequestedPackages}. + * + * <p> Supported packages in {@code newRequestedPackages} that have not been previously + * requested will be requested while supported packages not in {@code newRequestedPackages} + * but were previously requested will be cancelled. + * + * <p> This handles binding and unbinding to the health check service as required. + * + * <p> Note, calling this may modify {@code newRequestedPackages}. + * + * <p> Note, this method is not thread safe, all calls should be serialized. + */ + public void syncRequests(Set<String> newRequestedPackages) { + boolean enabled; + synchronized (mLock) { + enabled = mEnabled; + } + + if (!enabled) { + Slog.i(TAG, "Health checks disabled, no supported packages"); + // Call outside lock + mSupportedConsumer.accept(Collections.emptyList()); + return; + } + + getSupportedPackages(supportedPackageConfigs -> { + // Notify the watchdog without lock held + mSupportedConsumer.accept(supportedPackageConfigs); + getRequestedPackages(previousRequestedPackages -> { + synchronized (mLock) { + // Hold lock so requests and cancellations are sent atomically. + // It is important we don't mix requests from multiple threads. + + Set<String> supportedPackages = new ArraySet<>(); + for (PackageConfig config : supportedPackageConfigs) { + supportedPackages.add(config.getPackageName()); + } + // Note, this may modify newRequestedPackages + newRequestedPackages.retainAll(supportedPackages); + + // Cancel packages no longer requested + actOnDifference(previousRequestedPackages, + newRequestedPackages, p -> cancel(p)); + // Request packages not yet requested + actOnDifference(newRequestedPackages, + previousRequestedPackages, p -> request(p)); + + if (newRequestedPackages.isEmpty()) { + Slog.i(TAG, "No more health check requests, unbinding..."); + unbindService(); + return; + } + } + }); + }); + } + + private void actOnDifference(Collection<String> collection1, Collection<String> collection2, + Consumer<String> action) { + Iterator<String> iterator = collection1.iterator(); + while (iterator.hasNext()) { + String packageName = iterator.next(); + if (!collection2.contains(packageName)) { + action.accept(packageName); + } + } + } + + /** + * Requests an explicit health check for {@code packageName}. + * After this request, the callback registered on {@link #setCallbacks} can receive explicit + * health check passed results. + */ + private void request(String packageName) { + synchronized (mLock) { + if (!prepareServiceLocked("request health check for " + packageName)) { + return; + } + + Slog.i(TAG, "Requesting health check for package " + packageName); + try { + mRemoteService.request(packageName); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to request health check for package " + packageName, e); + } + } + } + + /** + * Cancels all explicit health checks for {@code packageName}. + * After this request, the callback registered on {@link #setCallbacks} can no longer receive + * explicit health check passed results. + */ + private void cancel(String packageName) { + synchronized (mLock) { + if (!prepareServiceLocked("cancel health check for " + packageName)) { + return; + } + + Slog.i(TAG, "Cancelling health check for package " + packageName); + try { + mRemoteService.cancel(packageName); + } catch (RemoteException e) { + // Do nothing, if the service is down, when it comes up, we will sync requests, + // if there's some other error, retrying wouldn't fix anyways. + Slog.w(TAG, "Failed to cancel health check for package " + packageName, e); + } + } + } + + /** + * Returns the packages that we can request explicit health checks for. + * The packages will be returned to the {@code consumer}. + */ + private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) { + synchronized (mLock) { + if (!prepareServiceLocked("get health check supported packages")) { + return; + } + + Slog.d(TAG, "Getting health check supported packages"); + try { + mRemoteService.getSupportedPackages(new RemoteCallback(result -> { + List<PackageConfig> packages = + result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class); + Slog.i(TAG, "Explicit health check supported packages " + packages); + consumer.accept(packages); + })); + } catch (RemoteException e) { + // Request failed, treat as if all observed packages are supported, if any packages + // expire during this period, we may incorrectly treat it as failing health checks + // even if we don't support health checks for the package. + Slog.w(TAG, "Failed to get health check supported packages", e); + } + } + } + + /** + * Returns the packages for which health checks are currently in progress. + * The packages will be returned to the {@code consumer}. + */ + private void getRequestedPackages(Consumer<List<String>> consumer) { + synchronized (mLock) { + if (!prepareServiceLocked("get health check requested packages")) { + return; + } + + Slog.d(TAG, "Getting health check requested packages"); + try { + mRemoteService.getRequestedPackages(new RemoteCallback(result -> { + List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES); + Slog.i(TAG, "Explicit health check requested packages " + packages); + consumer.accept(packages); + })); + } catch (RemoteException e) { + // Request failed, treat as if we haven't requested any packages, if any packages + // were actually requested, they will not be cancelled now. May be cancelled later + Slog.w(TAG, "Failed to get health check requested packages", e); + } + } + } + + /** + * Binds to the explicit health check service if the controller is enabled and + * not already bound. + */ + private void bindService() { + synchronized (mLock) { + if (!mEnabled || mConnection != null || mRemoteService != null) { + if (!mEnabled) { + Slog.i(TAG, "Not binding to service, service disabled"); + } else if (mRemoteService != null) { + Slog.i(TAG, "Not binding to service, service already connected"); + } else { + Slog.i(TAG, "Not binding to service, service already connecting"); + } + return; + } + ComponentName component = getServiceComponentNameLocked(); + if (component == null) { + Slog.wtf(TAG, "Explicit health check service not found"); + return; + } + + Intent intent = new Intent(); + intent.setComponent(component); + mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Slog.i(TAG, "Explicit health check service is connected " + name); + initState(service); + } + + @Override + @MainThread + public void onServiceDisconnected(ComponentName name) { + // Service crashed or process was killed, #onServiceConnected will be called. + // Don't need to re-bind. + Slog.i(TAG, "Explicit health check service is disconnected " + name); + synchronized (mLock) { + mRemoteService = null; + } + } + + @Override + public void onBindingDied(ComponentName name) { + // Application hosting service probably got updated + // Need to re-bind. + Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name); + unbindService(); + bindService(); + } + + @Override + public void onNullBinding(ComponentName name) { + // Should never happen. Service returned null from #onBind. + Slog.wtf(TAG, "Explicit health check service binding is null?? " + name); + } + }; + + mContext.bindServiceAsUser(intent, mConnection, + Context.BIND_AUTO_CREATE, UserHandle.SYSTEM); + Slog.i(TAG, "Explicit health check service is bound"); + } + } + + /** Unbinds the explicit health check service. */ + private void unbindService() { + synchronized (mLock) { + if (mRemoteService != null) { + mContext.unbindService(mConnection); + mRemoteService = null; + mConnection = null; + } + Slog.i(TAG, "Explicit health check service is unbound"); + } + } + + @GuardedBy("mLock") + @Nullable + private ServiceInfo getServiceInfoLocked() { + final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE); + final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA + | PackageManager.MATCH_SYSTEM_ONLY); + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + Slog.w(TAG, "No valid components found."); + return null; + } + return resolveInfo.serviceInfo; + } + + @GuardedBy("mLock") + @Nullable + private ComponentName getServiceComponentNameLocked() { + final ServiceInfo serviceInfo = getServiceInfoLocked(); + if (serviceInfo == null) { + return null; + } + + final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); + if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE + .equals(serviceInfo.permission)) { + Slog.w(TAG, name.flattenToShortString() + " does not require permission " + + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE); + return null; + } + return name; + } + + private void initState(IBinder service) { + synchronized (mLock) { + if (!mEnabled) { + Slog.w(TAG, "Attempting to connect disabled service?? Unbinding..."); + // Very unlikely, but we disabled the service after binding but before we connected + unbindService(); + return; + } + mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service); + try { + mRemoteService.setCallback(new RemoteCallback(result -> { + String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE); + if (!TextUtils.isEmpty(packageName)) { + if (mPassedConsumer == null) { + Slog.wtf(TAG, "Health check passed for package " + packageName + + "but no consumer registered."); + } else { + // Call without lock held + mPassedConsumer.accept(packageName); + } + } else { + Slog.wtf(TAG, "Empty package passed explicit health check?"); + } + })); + Slog.i(TAG, "Service initialized, syncing requests"); + } catch (RemoteException e) { + Slog.wtf(TAG, "Could not setCallback on explicit health check service"); + } + } + // Calling outside lock + mNotifySyncRunnable.run(); + } + + /** + * Prepares the health check service to receive requests. + * + * @return {@code true} if it is ready and we can proceed with a request, + * {@code false} otherwise. If it is not ready, and the service is enabled, + * we will bind and the request should be automatically attempted later. + */ + @GuardedBy("mLock") + private boolean prepareServiceLocked(String action) { + if (mRemoteService != null && mEnabled) { + return true; + } + Slog.i(TAG, "Service not ready to " + action + + (mEnabled ? ". Binding..." : ". Disabled")); + if (mEnabled) { + bindService(); + } + return false; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java new file mode 100644 index 000000000000..e4f07f9fc213 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java @@ -0,0 +1,2253 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static android.content.Intent.ACTION_REBOOT; +import static android.content.Intent.ACTION_SHUTDOWN; +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; +import static android.util.Xml.Encoding.UTF_8; + +import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecoveryEvents; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.os.SystemProperties; +import android.provider.DeviceConfig; +import android.sysprop.CrashRecoveryProperties; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.EventLog; +import android.util.IndentingPrintWriter; +import android.util.LongArrayQueue; +import android.util.Slog; +import android.util.Xml; +import android.util.XmlUtils; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.modules.utils.BackgroundThread; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * Monitors the health of packages on the system and notifies interested observers when packages + * fail. On failure, the registered observer with the least user impacting mitigation will + * be notified. + * @hide + */ +@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public class PackageWatchdog { + private static final String TAG = "PackageWatchdog"; + + static final String PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS = + "watchdog_trigger_failure_duration_millis"; + static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT = + "watchdog_trigger_failure_count"; + static final String PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED = + "watchdog_explicit_health_check_enabled"; + + // TODO: make the following values configurable via DeviceConfig + private static final long NATIVE_CRASH_POLLING_INTERVAL_MILLIS = + TimeUnit.SECONDS.toMillis(30); + private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10; + + + /** Reason for package failure could not be determined. */ + public static final int FAILURE_REASON_UNKNOWN = 0; + + /** The package had a native crash. */ + public static final int FAILURE_REASON_NATIVE_CRASH = 1; + + /** The package failed an explicit health check. */ + public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2; + + /** The app crashed. */ + public static final int FAILURE_REASON_APP_CRASH = 3; + + /** The app was not responding. */ + public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4; + + /** The device was boot looping. */ + public static final int FAILURE_REASON_BOOT_LOOP = 5; + + /** @hide */ + @IntDef(prefix = { "FAILURE_REASON_" }, value = { + FAILURE_REASON_UNKNOWN, + FAILURE_REASON_NATIVE_CRASH, + FAILURE_REASON_EXPLICIT_HEALTH_CHECK, + FAILURE_REASON_APP_CRASH, + FAILURE_REASON_APP_NOT_RESPONDING, + FAILURE_REASON_BOOT_LOOP + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FailureReasons {} + + // Duration to count package failures before it resets to 0 + @VisibleForTesting + static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS = + (int) TimeUnit.MINUTES.toMillis(1); + // Number of package failures within the duration above before we notify observers + @VisibleForTesting + static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5; + @VisibleForTesting + static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); + // Sliding window for tracking how many mitigation calls were made for a package. + @VisibleForTesting + static final long DEFAULT_DEESCALATION_WINDOW_MS = TimeUnit.HOURS.toMillis(1); + // Whether explicit health checks are enabled or not + private static final boolean DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED = true; + + @VisibleForTesting + static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5; + + static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10); + + // Time needed to apply mitigation + private static final String MITIGATION_WINDOW_MS = + "persist.device_config.configuration.mitigation_window_ms"; + @VisibleForTesting + static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5); + + // Threshold level at which or above user might experience significant disruption. + private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + "persist.device_config.configuration.major_user_impact_level_threshold"; + private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + + // Comma separated list of all packages exempt from user impact level threshold. If a package + // in the list is crash looping, all the mitigations including factory reset will be performed. + private static final String PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD = + "persist.device_config.configuration.packages_exempt_from_impact_level_threshold"; + + // Comma separated list of default packages exempt from user impact level threshold. + private static final String DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD = + "com.android.systemui"; + + private long mNumberOfNativeCrashPollsRemaining; + + private static final int DB_VERSION = 1; + private static final String TAG_PACKAGE_WATCHDOG = "package-watchdog"; + private static final String TAG_PACKAGE = "package"; + private static final String TAG_OBSERVER = "observer"; + private static final String ATTR_VERSION = "version"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_DURATION = "duration"; + private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration"; + private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check"; + private static final String ATTR_MITIGATION_CALLS = "mitigation-calls"; + private static final String ATTR_MITIGATION_COUNT = "mitigation-count"; + + // A file containing information about the current mitigation count in the case of a boot loop. + // This allows boot loop information to persist in the case of an fs-checkpoint being + // aborted. + private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt"; + + /** + * EventLog tags used when logging into the event log. Note the values must be sync with + * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct + * name translation. + */ + private static final int LOG_TAG_RESCUE_NOTE = 2900; + + private static final Object sPackageWatchdogLock = new Object(); + @GuardedBy("sPackageWatchdogLock") + private static PackageWatchdog sPackageWatchdog; + + private static final Object sLock = new Object(); + // System server context + private final Context mContext; + // Handler to run short running tasks + private final Handler mShortTaskHandler; + // Handler for processing IO and long running tasks + private final Handler mLongTaskHandler; + // Contains (observer-name -> observer-handle) that have ever been registered from + // previous boots. Observers with all packages expired are periodically pruned. + // It is saved to disk on system shutdown and repouplated on startup so it survives reboots. + @GuardedBy("sLock") + private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>(); + // File containing the XML data of monitored packages /data/system/package-watchdog.xml + private final AtomicFile mPolicyFile; + private final ExplicitHealthCheckController mHealthCheckController; + private final Runnable mSyncRequests = this::syncRequests; + private final Runnable mSyncStateWithScheduledReason = this::syncStateWithScheduledReason; + private final Runnable mSaveToFile = this::saveToFile; + private final SystemClock mSystemClock; + private final BootThreshold mBootThreshold; + private final DeviceConfig.OnPropertiesChangedListener + mOnPropertyChangedListener = this::onPropertyChanged; + + private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>(); + + // The set of packages that have been synced with the ExplicitHealthCheckController + @GuardedBy("sLock") + private Set<String> mRequestedHealthCheckPackages = new ArraySet<>(); + @GuardedBy("sLock") + private boolean mIsPackagesReady; + // Flag to control whether explicit health checks are supported or not + @GuardedBy("sLock") + private boolean mIsHealthCheckEnabled = DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED; + @GuardedBy("sLock") + private int mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; + @GuardedBy("sLock") + private int mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; + // SystemClock#uptimeMillis when we last executed #syncState + // 0 if no prune is scheduled. + @GuardedBy("sLock") + private long mUptimeAtLastStateSync; + // If true, sync explicit health check packages with the ExplicitHealthCheckController. + @GuardedBy("sLock") + private boolean mSyncRequired = false; + + @GuardedBy("sLock") + private long mLastMitigation = -1000000; + + @FunctionalInterface + @VisibleForTesting + interface SystemClock { + long uptimeMillis(); + } + + private PackageWatchdog(Context context) { + // Needs to be constructed inline + this(context, new AtomicFile( + new File(new File(Environment.getDataDirectory(), "system"), + "package-watchdog.xml")), + new Handler(Looper.myLooper()), BackgroundThread.getHandler(), + new ExplicitHealthCheckController(context), + android.os.SystemClock::uptimeMillis); + } + + /** + * Creates a PackageWatchdog that allows injecting dependencies. + */ + @VisibleForTesting + PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler, + Handler longTaskHandler, ExplicitHealthCheckController controller, + SystemClock clock) { + mContext = context; + mPolicyFile = policyFile; + mShortTaskHandler = shortTaskHandler; + mLongTaskHandler = longTaskHandler; + mHealthCheckController = controller; + mSystemClock = clock; + mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); + + loadFromFile(); + sPackageWatchdog = this; + } + + /** + * Creates or gets singleton instance of PackageWatchdog. + * + * @param context The system server context. + */ + public static @NonNull PackageWatchdog getInstance(@NonNull Context context) { + synchronized (sPackageWatchdogLock) { + if (sPackageWatchdog == null) { + new PackageWatchdog(context); + } + return sPackageWatchdog; + } + } + + /** + * Called during boot to notify when packages are ready on the device so we can start + * binding. + * @hide + */ + public void onPackagesReady() { + synchronized (sLock) { + mIsPackagesReady = true; + mHealthCheckController.setCallbacks(packageName -> onHealthCheckPassed(packageName), + packages -> onSupportedPackages(packages), + this::onSyncRequestNotified); + setPropertyChangedListenerLocked(); + updateConfigs(); + } + } + + /** + * Registers {@code observer} to listen for package failures. Add a new ObserverInternal for + * this observer if it does not already exist. + * For executing mitigations observers will receive callback on the given executor. + * + * <p>Observers are expected to call this on boot. It does not specify any packages but + * it will resume observing any packages requested from a previous boot. + * + * @param observer instance of {@link PackageHealthObserver} for observing package failures + * and boot loops. + * @param executor Executor for the thread on which observers would receive callbacks + */ + public void registerHealthObserver(@NonNull @CallbackExecutor Executor executor, + @NonNull PackageHealthObserver observer) { + synchronized (sLock) { + ObserverInternal internalObserver = mAllObservers.get(observer.getUniqueIdentifier()); + if (internalObserver != null) { + internalObserver.registeredObserver = observer; + internalObserver.observerExecutor = executor; + } else { + internalObserver = new ObserverInternal(observer.getUniqueIdentifier(), + new ArrayList<>()); + internalObserver.registeredObserver = observer; + internalObserver.observerExecutor = executor; + mAllObservers.put(observer.getUniqueIdentifier(), internalObserver); + syncState("added new observer"); + } + } + } + + /** + * Starts observing the health of the {@code packages} for {@code observer}. + * Note: Observer needs to be registered with {@link #registerHealthObserver} before calling + * this API. + * + * <p>If monitoring a package supporting explicit health check, at the end of the monitoring + * duration if {@link #onHealthCheckPassed} was never called, + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} will be called as if the + * package failed. + * + * <p>If {@code observer} is already monitoring a package in {@code packageNames}, + * the monitoring window of that package will be reset to {@code durationMs} and the health + * check state will be reset to a default. + * + * <p>The {@code observer} must be registered with {@link #registerHealthObserver} before + * calling this method. + * + * @param packageNames The list of packages to check. If this is empty, the call will be a + * no-op. + * + * @param timeoutMs The timeout after which Explicit Health Checks would not run. If this is + * less than 1, a default monitoring duration 2 days will be used. + * + * @throws IllegalStateException if the observer was not previously registered + */ + public void startExplicitHealthCheck(@NonNull List<String> packageNames, long timeoutMs, + @NonNull PackageHealthObserver observer) { + synchronized (sLock) { + if (!mAllObservers.containsKey(observer.getUniqueIdentifier())) { + Slog.wtf(TAG, "No observer found, need to register the observer: " + + observer.getUniqueIdentifier()); + throw new IllegalStateException("Observer not registered"); + } + } + if (packageNames.isEmpty()) { + Slog.wtf(TAG, "No packages to observe, " + observer.getUniqueIdentifier()); + return; + } + if (timeoutMs < 1) { + Slog.wtf(TAG, "Invalid duration " + timeoutMs + "ms for observer " + + observer.getUniqueIdentifier() + ". Not observing packages " + packageNames); + timeoutMs = DEFAULT_OBSERVING_DURATION_MS; + } + + List<MonitoredPackage> packages = new ArrayList<>(); + for (int i = 0; i < packageNames.size(); i++) { + // Health checks not available yet so health check state will start INACTIVE + MonitoredPackage pkg = newMonitoredPackage(packageNames.get(i), timeoutMs, false); + if (pkg != null) { + packages.add(pkg); + } else { + Slog.w(TAG, "Failed to create MonitoredPackage for pkg=" + packageNames.get(i)); + } + } + + if (packages.isEmpty()) { + return; + } + + // Sync before we add the new packages to the observers. This will #pruneObservers, + // causing any elapsed time to be deducted from all existing packages before we add new + // packages. This maintains the invariant that the elapsed time for ALL (new and existing) + // packages is the same. + mLongTaskHandler.post(() -> { + syncState("observing new packages"); + + synchronized (sLock) { + ObserverInternal oldObserver = mAllObservers.get(observer.getUniqueIdentifier()); + if (oldObserver == null) { + Slog.d(TAG, observer.getUniqueIdentifier() + " started monitoring health " + + "of packages " + packageNames); + mAllObservers.put(observer.getUniqueIdentifier(), + new ObserverInternal(observer.getUniqueIdentifier(), packages)); + } else { + Slog.d(TAG, observer.getUniqueIdentifier() + " added the following " + + "packages to monitor " + packageNames); + oldObserver.updatePackagesLocked(packages); + } + } + + // Sync after we add the new packages to the observers. We may have received packges + // requiring an earlier schedule than we are currently scheduled for. + syncState("updated observers"); + }); + + } + + /** + * Unregisters {@code observer} from listening to package failure. + * Additionally, this stops observing any packages that may have previously been observed + * even from a previous boot. + */ + public void unregisterHealthObserver(@NonNull PackageHealthObserver observer) { + mLongTaskHandler.post(() -> { + synchronized (sLock) { + mAllObservers.remove(observer.getUniqueIdentifier()); + } + syncState("unregistering observer: " + observer.getUniqueIdentifier()); + }); + } + + /** + * Called when a process fails due to a crash, ANR or explicit health check. + * + * <p>For each package contained in the process, one registered observer with the least user + * impact will be notified for mitigation. + * + * <p>This method could be called frequently if there is a severe problem on the device. + */ + public void notifyPackageFailure(@NonNull List<VersionedPackage> packages, + @FailureReasons int failureReason) { + if (packages == null) { + Slog.w(TAG, "Could not resolve a list of failing packages"); + return; + } + synchronized (sLock) { + final long now = mSystemClock.uptimeMillis(); + if (Flags.recoverabilityDetection()) { + if (now >= mLastMitigation + && (now - mLastMitigation) < getMitigationWindowMs()) { + Slog.i(TAG, "Skipping notifyPackageFailure mitigation"); + return; + } + } + } + mLongTaskHandler.post(() -> { + synchronized (sLock) { + if (mAllObservers.isEmpty()) { + return; + } + boolean requiresImmediateAction = (failureReason == FAILURE_REASON_NATIVE_CRASH + || failureReason == FAILURE_REASON_EXPLICIT_HEALTH_CHECK); + if (requiresImmediateAction) { + handleFailureImmediately(packages, failureReason); + } else { + for (int pIndex = 0; pIndex < packages.size(); pIndex++) { + VersionedPackage versionedPackage = packages.get(pIndex); + // Observer that will receive failure for versionedPackage + ObserverInternal currentObserverToNotify = null; + int currentObserverImpact = Integer.MAX_VALUE; + MonitoredPackage currentMonitoredPackage = null; + + // Find observer with least user impact + for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { + ObserverInternal observer = mAllObservers.valueAt(oIndex); + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null + && observer.notifyPackageFailureLocked( + versionedPackage.getPackageName())) { + MonitoredPackage p = observer.getMonitoredPackage( + versionedPackage.getPackageName()); + int mitigationCount = 1; + if (p != null) { + mitigationCount = p.getMitigationCountLocked() + 1; + } + int impact = registeredObserver.onHealthCheckFailed( + versionedPackage, failureReason, mitigationCount); + if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 + && impact < currentObserverImpact) { + currentObserverToNotify = observer; + currentObserverImpact = impact; + currentMonitoredPackage = p; + } + } + } + + // Execute action with least user impact + if (currentObserverToNotify != null) { + int mitigationCount; + if (currentMonitoredPackage != null) { + currentMonitoredPackage.noteMitigationCallLocked(); + mitigationCount = + currentMonitoredPackage.getMitigationCountLocked(); + } else { + mitigationCount = 1; + } + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, versionedPackage, + failureReason, currentObserverImpact, mitigationCount); + } else { + PackageHealthObserver registeredObserver = + currentObserverToNotify.registeredObserver; + currentObserverToNotify.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation( + versionedPackage, failureReason, mitigationCount)); + } + } + } + } + } + }); + } + + /** + * For native crashes or explicit health check failures, call directly into each observer to + * mitigate the error without going through failure threshold logic. + */ + @GuardedBy("sLock") + private void handleFailureImmediately(List<VersionedPackage> packages, + @FailureReasons int failureReason) { + VersionedPackage failingPackage = packages.size() > 0 ? packages.get(0) : null; + ObserverInternal currentObserverToNotify = null; + int currentObserverImpact = Integer.MAX_VALUE; + for (ObserverInternal observer: mAllObservers.values()) { + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null) { + int impact = registeredObserver.onHealthCheckFailed( + failingPackage, failureReason, 1); + if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 + && impact < currentObserverImpact) { + currentObserverToNotify = observer; + currentObserverImpact = impact; + } + } + } + if (currentObserverToNotify != null) { + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, failingPackage, failureReason, + currentObserverImpact, /*mitigationCount=*/ 1); + } else { + PackageHealthObserver registeredObserver = + currentObserverToNotify.registeredObserver; + currentObserverToNotify.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation(failingPackage, + failureReason, 1)); + + } + } + } + + private void maybeExecute(ObserverInternal currentObserverToNotify, + VersionedPackage versionedPackage, + @FailureReasons int failureReason, + int currentObserverImpact, + int mitigationCount) { + if (allowMitigations(currentObserverImpact, versionedPackage)) { + PackageHealthObserver registeredObserver; + synchronized (sLock) { + mLastMitigation = mSystemClock.uptimeMillis(); + registeredObserver = currentObserverToNotify.registeredObserver; + } + currentObserverToNotify.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation(versionedPackage, + failureReason, mitigationCount)); + } + } + + private boolean allowMitigations(int currentObserverImpact, + VersionedPackage versionedPackage) { + return currentObserverImpact < getUserImpactLevelLimit() + || getPackagesExemptFromImpactLevelThreshold().contains( + versionedPackage.getPackageName()); + } + + private long getMitigationWindowMs() { + return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS); + } + + + /** + * Called when the system server boots. If the system server is detected to be in a boot loop, + * query each observer and perform the mitigation action with the lowest user impact. + * + * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots + * are not counted in bootloop. + * @hide + */ + @SuppressWarnings("GuardedBy") + public void noteBoot() { + synchronized (sLock) { + // if boot count has reached threshold, start mitigation. + // We wait until threshold number of restarts only for the first time. Perform + // mitigations for every restart after that. + boolean mitigate = mBootThreshold.incrementAndTest(); + if (mitigate) { + if (!Flags.recoverabilityDetection()) { + mBootThreshold.reset(); + } + int mitigationCount = mBootThreshold.getMitigationCount() + 1; + ObserverInternal currentObserverToNotify = null; + int currentObserverImpact = Integer.MAX_VALUE; + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null) { + int impact = Flags.recoverabilityDetection() + ? registeredObserver.onBootLoop( + observer.getBootMitigationCount() + 1) + : registeredObserver.onBootLoop(mitigationCount); + if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 + && impact < currentObserverImpact) { + currentObserverToNotify = observer; + currentObserverImpact = impact; + } + } + } + + if (currentObserverToNotify != null) { + PackageHealthObserver registeredObserver = + currentObserverToNotify.registeredObserver; + if (Flags.recoverabilityDetection()) { + int currentObserverMitigationCount = + currentObserverToNotify.getBootMitigationCount() + 1; + currentObserverToNotify.setBootMitigationCount( + currentObserverMitigationCount); + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + currentObserverToNotify.observerExecutor + .execute(() -> registeredObserver.onExecuteBootLoopMitigation( + currentObserverMitigationCount)); + } else { + mBootThreshold.setMitigationCount(mitigationCount); + mBootThreshold.saveMitigationCountToMetadata(); + currentObserverToNotify.observerExecutor + .execute(() -> registeredObserver.onExecuteBootLoopMitigation( + mitigationCount)); + + } + } + } + } + } + + // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also + // avoid holding lock? + // This currently adds about 7ms extra to shutdown thread + /** @hide Writes the package information to file during shutdown. */ + public void writeNow() { + synchronized (sLock) { + // Must only run synchronous tasks as this runs on the ShutdownThread and no other + // thread is guaranteed to run during shutdown. + if (!mAllObservers.isEmpty()) { + mLongTaskHandler.removeCallbacks(mSaveToFile); + pruneObserversLocked(); + saveToFile(); + Slog.i(TAG, "Last write to update package durations"); + } + } + } + + /** + * Enables or disables explicit health checks. + * <p> If explicit health checks are enabled, the health check service is started. + * <p> If explicit health checks are disabled, pending explicit health check requests are + * passed and the health check service is stopped. + */ + private void setExplicitHealthCheckEnabled(boolean enabled) { + synchronized (sLock) { + mIsHealthCheckEnabled = enabled; + mHealthCheckController.setEnabled(enabled); + mSyncRequired = true; + // Prune to update internal state whenever health check is enabled/disabled + syncState("health check state " + (enabled ? "enabled" : "disabled")); + } + } + + /** + * This method should be only called on mShortTaskHandler, since it modifies + * {@link #mNumberOfNativeCrashPollsRemaining}. + */ + private void checkAndMitigateNativeCrashes() { + mNumberOfNativeCrashPollsRemaining--; + // Check if native watchdog reported a crash + if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) { + // We rollback all available low impact rollbacks when crash is unattributable + notifyPackageFailure(Collections.EMPTY_LIST, FAILURE_REASON_NATIVE_CRASH); + // we stop polling after an attempt to execute rollback, regardless of whether the + // attempt succeeds or not + } else { + if (mNumberOfNativeCrashPollsRemaining > 0) { + mShortTaskHandler.postDelayed(() -> checkAndMitigateNativeCrashes(), + NATIVE_CRASH_POLLING_INTERVAL_MILLIS); + } + } + } + + /** + * Since this method can eventually trigger a rollback, it should be called + * only once boot has completed {@code onBootCompleted} and not earlier, because the install + * session must be entirely completed before we try to rollback. + * @hide + */ + public void scheduleCheckAndMitigateNativeCrashes() { + Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check " + + "and mitigate native crashes"); + mShortTaskHandler.post(()->checkAndMitigateNativeCrashes()); + } + + private int getUserImpactLevelLimit() { + return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD, + DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD); + } + + private Set<String> getPackagesExemptFromImpactLevelThreshold() { + if (mPackagesExemptFromImpactLevelThreshold.isEmpty()) { + String packageNames = SystemProperties.get(PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD, + DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD); + return Set.of(packageNames.split("\\s*,\\s*")); + } + return mPackagesExemptFromImpactLevelThreshold; + } + + /** + * Indicates that a mitigation was successfully triggered or executed during + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or + * {@link PackageHealthObserver#onExecuteBootLoopMitigation}. + */ + public static final int MITIGATION_RESULT_SUCCESS = + ObserverMitigationResult.MITIGATION_RESULT_SUCCESS; + + /** + * Indicates that a mitigation executed during + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or + * {@link PackageHealthObserver#onExecuteBootLoopMitigation} was skipped. + */ + public static final int MITIGATION_RESULT_SKIPPED = + ObserverMitigationResult.MITIGATION_RESULT_SKIPPED; + + + /** + * Possible return values of the for mitigations executed during + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} and + * {@link PackageHealthObserver#onExecuteBootLoopMitigation}. + * @hide + */ + @Retention(SOURCE) + @IntDef(prefix = "MITIGATION_RESULT_", value = { + ObserverMitigationResult.MITIGATION_RESULT_SUCCESS, + ObserverMitigationResult.MITIGATION_RESULT_SKIPPED, + }) + public @interface ObserverMitigationResult { + int MITIGATION_RESULT_SUCCESS = 1; + int MITIGATION_RESULT_SKIPPED = 2; + } + + /** + * The minimum value that can be returned by any observer. + * It represents that no mitigations were available. + */ + public static final int USER_IMPACT_THRESHOLD_NONE = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + + /** + * The mitigation impact beyond which the user will start noticing the mitigations. + */ + public static final int USER_IMPACT_THRESHOLD_MEDIUM = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20; + + /** + * The mitigation impact beyond which the user impact is severely high. + */ + public static final int USER_IMPACT_THRESHOLD_HIGH = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + + /** + * Possible severity values of the user impact of a + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation}. + * @hide + */ + @Retention(SOURCE) + @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_40, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_70, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_75, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_90, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_100}) + public @interface PackageHealthObserverImpact { + /** No action to take. */ + int USER_IMPACT_LEVEL_0 = 0; + /* Action has low user impact, user of a device will barely notice. */ + int USER_IMPACT_LEVEL_10 = 10; + /* Actions having medium user impact, user of a device will likely notice. */ + int USER_IMPACT_LEVEL_20 = 20; + int USER_IMPACT_LEVEL_30 = 30; + int USER_IMPACT_LEVEL_40 = 40; + int USER_IMPACT_LEVEL_50 = 50; + int USER_IMPACT_LEVEL_70 = 70; + /* Action has high user impact, a last resort, user of a device will be very frustrated. */ + int USER_IMPACT_LEVEL_71 = 71; + int USER_IMPACT_LEVEL_75 = 75; + int USER_IMPACT_LEVEL_80 = 80; + int USER_IMPACT_LEVEL_90 = 90; + int USER_IMPACT_LEVEL_100 = 100; + } + + /** Register instances of this interface to receive notifications on package failure. */ + @SuppressLint({"CallbackName"}) + public interface PackageHealthObserver { + /** + * Called when health check fails for the {@code versionedPackage}. + * Note: if the returned user impact is higher than {@link #USER_IMPACT_THRESHOLD_HIGH}, + * then {@link #onExecuteHealthCheckMitigation} would be called only in severe device + * conditions like boot-loop or network failure. + * + * @param versionedPackage the package that is failing. This may be null if a native + * service is crashing. + * @param failureReason the type of failure that is occurring. + * @param mitigationCount the number of times mitigation has been called for this package + * (including this time). + * + * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express + * the impact of mitigation on the user in {@link #onExecuteHealthCheckMitigation}. + * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available. + */ + @PackageHealthObserverImpact int onHealthCheckFailed( + @Nullable VersionedPackage versionedPackage, + @FailureReasons int failureReason, + int mitigationCount); + + /** + * This would be called after {@link #onHealthCheckFailed}. + * This is called only if current observer returned least impact mitigation for failed + * health check. + * + * @param versionedPackage the package that is failing. This may be null if a native + * service is crashing. + * @param failureReason the type of failure that is occurring. + * @param mitigationCount the number of times mitigation has been called for this package + * (including this time). + * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful, + * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped. + */ + @ObserverMitigationResult int onExecuteHealthCheckMitigation( + @Nullable VersionedPackage versionedPackage, + @FailureReasons int failureReason, int mitigationCount); + + + /** + * Called when the system server has booted several times within a window of time, defined + * by {@link #mBootThreshold} + * + * @param mitigationCount the number of times mitigation has been attempted for this + * boot loop (including this time). + * + * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express + * the impact of mitigation on the user in {@link #onExecuteBootLoopMitigation}. + * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available. + */ + default @PackageHealthObserverImpact int onBootLoop(int mitigationCount) { + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + + /** + * This would be called after {@link #onBootLoop}. + * This is called only if current observer returned least impact mitigation for fixing + * boot loop. + * + * @param mitigationCount the number of times mitigation has been attempted for this + * boot loop (including this time). + * + * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful, + * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped. + */ + default @ObserverMitigationResult int onExecuteBootLoopMitigation(int mitigationCount) { + return ObserverMitigationResult.MITIGATION_RESULT_SKIPPED; + } + + // TODO(b/120598832): Ensure uniqueness? + /** + * Identifier for the observer, should not change across device updates otherwise the + * watchdog may drop observing packages with the old name. + */ + @NonNull String getUniqueIdentifier(); + + /** + * An observer will not be pruned if this is set, even if the observer is not explicitly + * monitoring any packages. + */ + default boolean isPersistent() { + return false; + } + + /** + * Returns {@code true} if this observer wishes to observe the given package, {@code false} + * otherwise. + * Any failing package can be passed on to the observer. Currently the packages that have + * ANRs and perform {@link android.service.watchdog.ExplicitHealthCheckService} are being + * passed to observers in these API. + * + * <p> A persistent observer may choose to start observing certain failing packages, even if + * it has not explicitly asked to watch the package with {@link #startExplicitHealthCheck}. + */ + default boolean mayObservePackage(@NonNull String packageName) { + return false; + } + } + + @VisibleForTesting + long getTriggerFailureCount() { + synchronized (sLock) { + return mTriggerFailureCount; + } + } + + @VisibleForTesting + long getTriggerFailureDurationMs() { + synchronized (sLock) { + return mTriggerFailureDurationMs; + } + } + + /** + * Serializes and syncs health check requests with the {@link ExplicitHealthCheckController}. + */ + private void syncRequestsAsync() { + mShortTaskHandler.removeCallbacks(mSyncRequests); + mShortTaskHandler.post(mSyncRequests); + } + + /** + * Syncs health check requests with the {@link ExplicitHealthCheckController}. + * Calls to this must be serialized. + * + * @see #syncRequestsAsync + */ + private void syncRequests() { + boolean syncRequired = false; + synchronized (sLock) { + if (mIsPackagesReady) { + Set<String> packages = getPackagesPendingHealthChecksLocked(); + if (mSyncRequired || !packages.equals(mRequestedHealthCheckPackages) + || packages.isEmpty()) { + syncRequired = true; + mRequestedHealthCheckPackages = packages; + } + } // else, we will sync requests when packages become ready + } + + // Call outside lock to avoid holding lock when calling into the controller. + if (syncRequired) { + Slog.i(TAG, "Syncing health check requests for packages: " + + mRequestedHealthCheckPackages); + mHealthCheckController.syncRequests(mRequestedHealthCheckPackages); + mSyncRequired = false; + } + } + + /** + * Updates the observers monitoring {@code packageName} that explicit health check has passed. + * + * <p> This update is strictly for registered observers at the time of the call + * Observers that register after this signal will have no knowledge of prior signals and will + * effectively behave as if the explicit health check hasn't passed for {@code packageName}. + * + * <p> {@code packageName} can still be considered failed if reported by + * {@link #notifyPackageFailureLocked} before the package expires. + * + * <p> Triggered by components outside the system server when they are fully functional after an + * update. + */ + private void onHealthCheckPassed(String packageName) { + Slog.i(TAG, "Health check passed for package: " + packageName); + boolean isStateChanged = false; + + synchronized (sLock) { + for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) { + ObserverInternal observer = mAllObservers.valueAt(observerIdx); + MonitoredPackage monitoredPackage = observer.getMonitoredPackage(packageName); + + if (monitoredPackage != null) { + int oldState = monitoredPackage.getHealthCheckStateLocked(); + int newState = monitoredPackage.tryPassHealthCheckLocked(); + isStateChanged |= oldState != newState; + } + } + } + + if (isStateChanged) { + syncState("health check passed for " + packageName); + } + } + + private void onSupportedPackages(List<PackageConfig> supportedPackages) { + boolean isStateChanged = false; + + Map<String, Long> supportedPackageTimeouts = new ArrayMap<>(); + Iterator<PackageConfig> it = supportedPackages.iterator(); + while (it.hasNext()) { + PackageConfig info = it.next(); + supportedPackageTimeouts.put(info.getPackageName(), info.getHealthCheckTimeoutMillis()); + } + + synchronized (sLock) { + Slog.d(TAG, "Received supported packages " + supportedPackages); + Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); + while (oit.hasNext()) { + Iterator<MonitoredPackage> pit = oit.next().getMonitoredPackages() + .values().iterator(); + while (pit.hasNext()) { + MonitoredPackage monitoredPackage = pit.next(); + String packageName = monitoredPackage.getName(); + int oldState = monitoredPackage.getHealthCheckStateLocked(); + int newState; + + if (supportedPackageTimeouts.containsKey(packageName)) { + // Supported packages become ACTIVE if currently INACTIVE + newState = monitoredPackage.setHealthCheckActiveLocked( + supportedPackageTimeouts.get(packageName)); + } else { + // Unsupported packages are marked as PASSED unless already FAILED + newState = monitoredPackage.tryPassHealthCheckLocked(); + } + isStateChanged |= oldState != newState; + } + } + } + + if (isStateChanged) { + syncState("updated health check supported packages " + supportedPackages); + } + } + + private void onSyncRequestNotified() { + synchronized (sLock) { + mSyncRequired = true; + syncRequestsAsync(); + } + } + + @GuardedBy("sLock") + private Set<String> getPackagesPendingHealthChecksLocked() { + Set<String> packages = new ArraySet<>(); + Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); + while (oit.hasNext()) { + ObserverInternal observer = oit.next(); + Iterator<MonitoredPackage> pit = + observer.getMonitoredPackages().values().iterator(); + while (pit.hasNext()) { + MonitoredPackage monitoredPackage = pit.next(); + String packageName = monitoredPackage.getName(); + if (monitoredPackage.isPendingHealthChecksLocked()) { + packages.add(packageName); + } + } + } + return packages; + } + + /** + * Syncs the state of the observers. + * + * <p> Prunes all observers, saves new state to disk, syncs health check requests with the + * health check service and schedules the next state sync. + */ + private void syncState(String reason) { + synchronized (sLock) { + Slog.i(TAG, "Syncing state, reason: " + reason); + pruneObserversLocked(); + + saveToFileAsync(); + syncRequestsAsync(); + + // Done syncing state, schedule the next state sync + scheduleNextSyncStateLocked(); + } + } + + private void syncStateWithScheduledReason() { + syncState("scheduled"); + } + + @GuardedBy("sLock") + private void scheduleNextSyncStateLocked() { + long durationMs = getNextStateSyncMillisLocked(); + mShortTaskHandler.removeCallbacks(mSyncStateWithScheduledReason); + if (durationMs == Long.MAX_VALUE) { + Slog.i(TAG, "Cancelling state sync, nothing to sync"); + mUptimeAtLastStateSync = 0; + } else { + mUptimeAtLastStateSync = mSystemClock.uptimeMillis(); + mShortTaskHandler.postDelayed(mSyncStateWithScheduledReason, durationMs); + } + } + + /** + * Returns the next duration in millis to sync the watchdog state. + * + * @returns Long#MAX_VALUE if there are no observed packages. + */ + @GuardedBy("sLock") + private long getNextStateSyncMillisLocked() { + long shortestDurationMs = Long.MAX_VALUE; + for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { + ArrayMap<String, MonitoredPackage> packages = mAllObservers.valueAt(oIndex) + .getMonitoredPackages(); + for (int pIndex = 0; pIndex < packages.size(); pIndex++) { + MonitoredPackage mp = packages.valueAt(pIndex); + long duration = mp.getShortestScheduleDurationMsLocked(); + if (duration < shortestDurationMs) { + shortestDurationMs = duration; + } + } + } + return shortestDurationMs; + } + + /** + * Removes {@code elapsedMs} milliseconds from all durations on monitored packages + * and updates other internal state. + */ + @GuardedBy("sLock") + private void pruneObserversLocked() { + long elapsedMs = mUptimeAtLastStateSync == 0 + ? 0 : mSystemClock.uptimeMillis() - mUptimeAtLastStateSync; + if (elapsedMs <= 0) { + Slog.i(TAG, "Not pruning observers, elapsed time: " + elapsedMs + "ms"); + return; + } + + Iterator<ObserverInternal> it = mAllObservers.values().iterator(); + while (it.hasNext()) { + ObserverInternal observer = it.next(); + Set<MonitoredPackage> failedPackages = + observer.prunePackagesLocked(elapsedMs); + if (!failedPackages.isEmpty()) { + onHealthCheckFailed(observer, failedPackages); + } + if (observer.getMonitoredPackages().isEmpty() && (observer.registeredObserver == null + || !observer.registeredObserver.isPersistent())) { + Slog.i(TAG, "Discarding observer " + observer.name + ". All packages expired"); + it.remove(); + } + } + } + + private void onHealthCheckFailed(ObserverInternal observer, + Set<MonitoredPackage> failedPackages) { + mLongTaskHandler.post(() -> { + synchronized (sLock) { + PackageHealthObserver registeredObserver = observer.registeredObserver; + if (registeredObserver != null) { + Iterator<MonitoredPackage> it = failedPackages.iterator(); + while (it.hasNext()) { + VersionedPackage versionedPkg = getVersionedPackage(it.next().getName()); + if (versionedPkg != null) { + Slog.i(TAG, + "Explicit health check failed for package " + versionedPkg); + observer.observerExecutor.execute(() -> + registeredObserver.onExecuteHealthCheckMitigation(versionedPkg, + PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK, + 1)); + } + } + } + } + }); + } + + /** + * Gets PackageInfo for the given package. Matches any user and apex. + * + * @throws PackageManager.NameNotFoundException if no such package is installed. + */ + private PackageInfo getPackageInfo(String packageName) + throws PackageManager.NameNotFoundException { + PackageManager pm = mContext.getPackageManager(); + try { + // The MATCH_ANY_USER flag doesn't mix well with the MATCH_APEX + // flag, so make two separate attempts to get the package info. + // We don't need both flags at the same time because we assume + // apex files are always installed for all users. + return pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER); + } catch (PackageManager.NameNotFoundException e) { + return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); + } + } + + @Nullable + private VersionedPackage getVersionedPackage(String packageName) { + final PackageManager pm = mContext.getPackageManager(); + if (pm == null || TextUtils.isEmpty(packageName)) { + return null; + } + try { + final long versionCode = getPackageInfo(packageName).getLongVersionCode(); + return new VersionedPackage(packageName, versionCode); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * Loads mAllObservers from file. + * + * <p>Note that this is <b>not</b> thread safe and should only called be called + * from the constructor. + */ + private void loadFromFile() { + InputStream infile = null; + mAllObservers.clear(); + try { + infile = mPolicyFile.openRead(); + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(infile, UTF_8.name()); + XmlUtils.beginDocument(parser, TAG_PACKAGE_WATCHDOG); + int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + ObserverInternal observer = ObserverInternal.read(parser, this); + if (observer != null) { + mAllObservers.put(observer.name, observer); + } + } + } catch (FileNotFoundException e) { + // Nothing to monitor + } catch (Exception e) { + Slog.wtf(TAG, "Unable to read monitored packages, deleting file", e); + mPolicyFile.delete(); + } finally { + IoUtils.closeQuietly(infile); + } + } + + private void onPropertyChanged(DeviceConfig.Properties properties) { + try { + updateConfigs(); + } catch (Exception ignore) { + Slog.w(TAG, "Failed to reload device config changes"); + } + } + + /** Adds a {@link DeviceConfig#OnPropertiesChangedListener}. */ + private void setPropertyChangedListenerLocked() { + DeviceConfig.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_ROLLBACK, + mContext.getMainExecutor(), + mOnPropertyChangedListener); + } + + @VisibleForTesting + void removePropertyChangedListener() { + DeviceConfig.removeOnPropertiesChangedListener(mOnPropertyChangedListener); + } + + /** + * Health check is enabled or disabled after reading the flags + * from DeviceConfig. + */ + @VisibleForTesting + void updateConfigs() { + synchronized (sLock) { + mTriggerFailureCount = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + DEFAULT_TRIGGER_FAILURE_COUNT); + if (mTriggerFailureCount <= 0) { + mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; + } + + mTriggerFailureDurationMs = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, + DEFAULT_TRIGGER_FAILURE_DURATION_MS); + if (mTriggerFailureDurationMs <= 0) { + mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; + } + + setExplicitHealthCheckEnabled(DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED, + DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED)); + } + } + + /** + * Persists mAllObservers to file. Threshold information is ignored. + */ + private boolean saveToFile() { + Slog.i(TAG, "Saving observer state to file"); + synchronized (sLock) { + FileOutputStream stream; + try { + stream = mPolicyFile.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Cannot update monitored packages", e); + return false; + } + + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_PACKAGE_WATCHDOG); + out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION)); + for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { + mAllObservers.valueAt(oIndex).writeLocked(out); + } + out.endTag(null, TAG_PACKAGE_WATCHDOG); + out.endDocument(); + mPolicyFile.finishWrite(stream); + return true; + } catch (IOException e) { + Slog.w(TAG, "Failed to save monitored packages, restoring backup", e); + mPolicyFile.failWrite(stream); + return false; + } + } + } + + private void saveToFileAsync() { + if (!mLongTaskHandler.hasCallbacks(mSaveToFile)) { + mLongTaskHandler.post(mSaveToFile); + } + } + + /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */ + public static String longArrayQueueToString(LongArrayQueue queue) { + if (queue.size() > 0) { + StringBuilder sb = new StringBuilder(); + sb.append(queue.get(0)); + for (int i = 1; i < queue.size(); i++) { + sb.append(","); + sb.append(queue.get(i)); + } + return sb.toString(); + } + return ""; + } + + /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */ + public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) { + LongArrayQueue result = new LongArrayQueue(); + if (!TextUtils.isEmpty(commaSeparatedValues)) { + String[] values = commaSeparatedValues.split(","); + for (String value : values) { + result.addLast(Long.parseLong(value)); + } + } + return result; + } + + + /** Dump status of every observer in mAllObservers. */ + public void dump(@NonNull PrintWriter pw) { + if (Flags.synchronousRebootInRescueParty() && isRecoveryTriggeredReboot()) { + dumpInternal(pw); + } else { + synchronized (sLock) { + dumpInternal(pw); + } + } + } + + /** + * Check if we're currently attempting to reboot during mitigation. This method must return + * true if triggered reboot early during a boot loop, since the device will not be fully booted + * at this time. + */ + public static boolean isRecoveryTriggeredReboot() { + return isFactoryResetPropertySet() || isRebootPropertySet(); + } + + private static boolean isFactoryResetPropertySet() { + return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); + } + + private static boolean isRebootPropertySet() { + return CrashRecoveryProperties.attemptingReboot().orElse(false); + } + + private void dumpInternal(@NonNull PrintWriter pw) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + ipw.println("Package Watchdog status"); + ipw.increaseIndent(); + synchronized (sLock) { + for (String observerName : mAllObservers.keySet()) { + ipw.println("Observer name: " + observerName); + ipw.increaseIndent(); + ObserverInternal observerInternal = mAllObservers.get(observerName); + observerInternal.dump(ipw); + ipw.decreaseIndent(); + } + } + ipw.decreaseIndent(); + dumpCrashRecoveryEvents(ipw); + } + + @VisibleForTesting + @GuardedBy("sLock") + void registerObserverInternal(ObserverInternal observerInternal) { + mAllObservers.put(observerInternal.name, observerInternal); + } + + /** + * Represents an observer monitoring a set of packages along with the failure thresholds for + * each package. + * + * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing + * instances of this class. + */ + static class ObserverInternal { + public final String name; + @GuardedBy("sLock") + private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>(); + @Nullable + @GuardedBy("sLock") + public PackageHealthObserver registeredObserver; + public Executor observerExecutor; + private int mMitigationCount; + + ObserverInternal(String name, List<MonitoredPackage> packages) { + this(name, packages, /*mitigationCount=*/ 0); + } + + ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) { + this.name = name; + updatePackagesLocked(packages); + this.mMitigationCount = mitigationCount; + } + + /** + * Writes important {@link MonitoredPackage} details for this observer to file. + * Does not persist any package failure thresholds. + */ + @GuardedBy("sLock") + public boolean writeLocked(XmlSerializer out) { + try { + out.startTag(null, TAG_OBSERVER); + out.attribute(null, ATTR_NAME, name); + if (Flags.recoverabilityDetection()) { + out.attribute(null, ATTR_MITIGATION_COUNT, Integer.toString(mMitigationCount)); + } + for (int i = 0; i < mPackages.size(); i++) { + MonitoredPackage p = mPackages.valueAt(i); + p.writeLocked(out); + } + out.endTag(null, TAG_OBSERVER); + return true; + } catch (IOException e) { + Slog.w(TAG, "Cannot save observer", e); + return false; + } + } + + public int getBootMitigationCount() { + return mMitigationCount; + } + + public void setBootMitigationCount(int mitigationCount) { + mMitigationCount = mitigationCount; + } + + @GuardedBy("sLock") + public void updatePackagesLocked(List<MonitoredPackage> packages) { + for (int pIndex = 0; pIndex < packages.size(); pIndex++) { + MonitoredPackage p = packages.get(pIndex); + MonitoredPackage existingPackage = getMonitoredPackage(p.getName()); + if (existingPackage != null) { + existingPackage.updateHealthCheckDuration(p.mDurationMs); + } else { + putMonitoredPackage(p); + } + } + } + + /** + * Reduces the monitoring durations of all packages observed by this observer by + * {@code elapsedMs}. If any duration is less than 0, the package is removed from + * observation. If any health check duration is less than 0, the health check result + * is evaluated. + * + * @return a {@link Set} of packages that were removed from the observer without explicit + * health check passing, or an empty list if no package expired for which an explicit health + * check was still pending + */ + @GuardedBy("sLock") + private Set<MonitoredPackage> prunePackagesLocked(long elapsedMs) { + Set<MonitoredPackage> failedPackages = new ArraySet<>(); + Iterator<MonitoredPackage> it = mPackages.values().iterator(); + while (it.hasNext()) { + MonitoredPackage p = it.next(); + int oldState = p.getHealthCheckStateLocked(); + int newState = p.handleElapsedTimeLocked(elapsedMs); + if (oldState != HealthCheckState.FAILED + && newState == HealthCheckState.FAILED) { + Slog.i(TAG, "Package " + p.getName() + " failed health check"); + failedPackages.add(p); + } + if (p.isExpiredLocked()) { + it.remove(); + } + } + return failedPackages; + } + + /** + * Increments failure counts of {@code packageName}. + * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise + * @hide + */ + @GuardedBy("sLock") + public boolean notifyPackageFailureLocked(String packageName) { + if (getMonitoredPackage(packageName) == null && registeredObserver.isPersistent() + && registeredObserver.mayObservePackage(packageName)) { + putMonitoredPackage(sPackageWatchdog.newMonitoredPackage( + packageName, DEFAULT_OBSERVING_DURATION_MS, false)); + } + MonitoredPackage p = getMonitoredPackage(packageName); + if (p != null) { + return p.onFailureLocked(); + } + return false; + } + + /** + * Returns the map of packages monitored by this observer. + * + * @return a mapping of package names to {@link MonitoredPackage} objects. + */ + @GuardedBy("sLock") + public ArrayMap<String, MonitoredPackage> getMonitoredPackages() { + return mPackages; + } + + /** + * Returns the {@link MonitoredPackage} associated with a given package name if the + * package is being monitored by this observer. + * + * @param packageName: the name of the package. + * @return the {@link MonitoredPackage} object associated with the package name if one + * exists, {@code null} otherwise. + */ + @GuardedBy("sLock") + @Nullable + public MonitoredPackage getMonitoredPackage(String packageName) { + return mPackages.get(packageName); + } + + /** + * Associates a {@link MonitoredPackage} with the observer. + * + * @param p: the {@link MonitoredPackage} to store. + */ + @GuardedBy("sLock") + public void putMonitoredPackage(MonitoredPackage p) { + mPackages.put(p.getName(), p); + } + + /** + * Returns one ObserverInternal from the {@code parser} and advances its state. + * + * <p>Note that this method is <b>not</b> thread safe. It should only be called from + * #loadFromFile which in turn is only called on construction of the + * singleton PackageWatchdog. + **/ + public static ObserverInternal read(XmlPullParser parser, PackageWatchdog watchdog) { + String observerName = null; + int observerMitigationCount = 0; + if (TAG_OBSERVER.equals(parser.getName())) { + observerName = parser.getAttributeValue(null, ATTR_NAME); + if (TextUtils.isEmpty(observerName)) { + Slog.wtf(TAG, "Unable to read observer name"); + return null; + } + } + List<MonitoredPackage> packages = new ArrayList<>(); + int innerDepth = parser.getDepth(); + try { + if (Flags.recoverabilityDetection()) { + try { + observerMitigationCount = Integer.parseInt( + parser.getAttributeValue(null, ATTR_MITIGATION_COUNT)); + } catch (Exception e) { + Slog.i( + TAG, + "ObserverInternal mitigation count was not present."); + } + } + while (XmlUtils.nextElementWithin(parser, innerDepth)) { + if (TAG_PACKAGE.equals(parser.getName())) { + try { + MonitoredPackage pkg = watchdog.parseMonitoredPackage(parser); + if (pkg != null) { + packages.add(pkg); + } + } catch (NumberFormatException e) { + Slog.wtf(TAG, "Skipping package for observer " + observerName, e); + continue; + } + } + } + } catch (XmlPullParserException | IOException e) { + Slog.wtf(TAG, "Unable to read observer " + observerName, e); + return null; + } + if (packages.isEmpty()) { + return null; + } + return new ObserverInternal(observerName, packages, observerMitigationCount); + } + + /** Dumps information about this observer and the packages it watches. */ + public void dump(IndentingPrintWriter pw) { + boolean isPersistent = registeredObserver != null && registeredObserver.isPersistent(); + pw.println("Persistent: " + isPersistent); + for (String packageName : mPackages.keySet()) { + MonitoredPackage p = getMonitoredPackage(packageName); + pw.println(packageName + ": "); + pw.increaseIndent(); + pw.println("# Failures: " + p.mFailureHistory.size()); + pw.println("Monitoring duration remaining: " + p.mDurationMs + "ms"); + pw.println("Explicit health check duration: " + p.mHealthCheckDurationMs + "ms"); + pw.println("Health check state: " + p.toString(p.mHealthCheckState)); + pw.decreaseIndent(); + } + } + } + + /** @hide */ + @Retention(SOURCE) + @IntDef(value = { + HealthCheckState.ACTIVE, + HealthCheckState.INACTIVE, + HealthCheckState.PASSED, + HealthCheckState.FAILED}) + public @interface HealthCheckState { + // The package has not passed health check but has requested a health check + int ACTIVE = 0; + // The package has not passed health check and has not requested a health check + int INACTIVE = 1; + // The package has passed health check + int PASSED = 2; + // The package has failed health check + int FAILED = 3; + } + + MonitoredPackage newMonitoredPackage( + String name, long durationMs, boolean hasPassedHealthCheck) { + return newMonitoredPackage(name, durationMs, Long.MAX_VALUE, hasPassedHealthCheck, + new LongArrayQueue()); + } + + MonitoredPackage newMonitoredPackage(String name, long durationMs, long healthCheckDurationMs, + boolean hasPassedHealthCheck, LongArrayQueue mitigationCalls) { + return new MonitoredPackage(name, durationMs, healthCheckDurationMs, + hasPassedHealthCheck, mitigationCalls); + } + + MonitoredPackage parseMonitoredPackage(XmlPullParser parser) + throws XmlPullParserException { + String packageName = parser.getAttributeValue(null, ATTR_NAME); + long duration = Long.parseLong(parser.getAttributeValue(null, ATTR_DURATION)); + long healthCheckDuration = Long.parseLong(parser.getAttributeValue(null, + ATTR_EXPLICIT_HEALTH_CHECK_DURATION)); + boolean hasPassedHealthCheck = Boolean.parseBoolean(parser.getAttributeValue(null, + ATTR_PASSED_HEALTH_CHECK)); + LongArrayQueue mitigationCalls = parseLongArrayQueue( + parser.getAttributeValue(null, ATTR_MITIGATION_CALLS)); + return newMonitoredPackage(packageName, + duration, healthCheckDuration, hasPassedHealthCheck, mitigationCalls); + } + + /** + * Represents a package and its health check state along with the time + * it should be monitored for. + * + * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing + * instances of this class. + */ + class MonitoredPackage { + private final String mPackageName; + // Times when package failures happen sorted in ascending order + @GuardedBy("sLock") + private final LongArrayQueue mFailureHistory = new LongArrayQueue(); + // Times when an observer was called to mitigate this package's failure. Sorted in + // ascending order. + @GuardedBy("sLock") + private final LongArrayQueue mMitigationCalls; + // One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after + // methods that could change the health check state: handleElapsedTimeLocked and + // tryPassHealthCheckLocked + private int mHealthCheckState = HealthCheckState.INACTIVE; + // Whether an explicit health check has passed. + // This value in addition with mHealthCheckDurationMs determines the health check state + // of the package, see #getHealthCheckStateLocked + @GuardedBy("sLock") + private boolean mHasPassedHealthCheck; + // System uptime duration to monitor package. + @GuardedBy("sLock") + private long mDurationMs; + // System uptime duration to check the result of an explicit health check + // Initially, MAX_VALUE until we get a value from the health check service + // and request health checks. + // This value in addition with mHasPassedHealthCheck determines the health check state + // of the package, see #getHealthCheckStateLocked + @GuardedBy("sLock") + private long mHealthCheckDurationMs = Long.MAX_VALUE; + + MonitoredPackage(String packageName, long durationMs, + long healthCheckDurationMs, boolean hasPassedHealthCheck, + LongArrayQueue mitigationCalls) { + mPackageName = packageName; + mDurationMs = durationMs; + mHealthCheckDurationMs = healthCheckDurationMs; + mHasPassedHealthCheck = hasPassedHealthCheck; + mMitigationCalls = mitigationCalls; + updateHealthCheckStateLocked(); + } + + /** Writes the salient fields to disk using {@code out}. + * @hide + */ + @GuardedBy("sLock") + public void writeLocked(XmlSerializer out) throws IOException { + out.startTag(null, TAG_PACKAGE); + out.attribute(null, ATTR_NAME, getName()); + out.attribute(null, ATTR_DURATION, Long.toString(mDurationMs)); + out.attribute(null, ATTR_EXPLICIT_HEALTH_CHECK_DURATION, + Long.toString(mHealthCheckDurationMs)); + out.attribute(null, ATTR_PASSED_HEALTH_CHECK, Boolean.toString(mHasPassedHealthCheck)); + LongArrayQueue normalizedCalls = normalizeMitigationCalls(); + out.attribute(null, ATTR_MITIGATION_CALLS, longArrayQueueToString(normalizedCalls)); + out.endTag(null, TAG_PACKAGE); + } + + /** + * Increment package failures or resets failure count depending on the last package failure. + * + * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise + */ + @GuardedBy("sLock") + public boolean onFailureLocked() { + // Sliding window algorithm: find out if there exists a window containing failures >= + // mTriggerFailureCount. + final long now = mSystemClock.uptimeMillis(); + mFailureHistory.addLast(now); + while (now - mFailureHistory.peekFirst() > mTriggerFailureDurationMs) { + // Prune values falling out of the window + mFailureHistory.removeFirst(); + } + boolean failed = mFailureHistory.size() >= mTriggerFailureCount; + if (failed) { + mFailureHistory.clear(); + } + return failed; + } + + /** + * Notes the timestamp of a mitigation call into the observer. + */ + @GuardedBy("sLock") + public void noteMitigationCallLocked() { + mMitigationCalls.addLast(mSystemClock.uptimeMillis()); + } + + /** + * Prunes any mitigation calls outside of the de-escalation window, and returns the + * number of calls that are in the window afterwards. + * + * @return the number of mitigation calls made in the de-escalation window. + */ + @GuardedBy("sLock") + public int getMitigationCountLocked() { + try { + final long now = mSystemClock.uptimeMillis(); + while (now - mMitigationCalls.peekFirst() > DEFAULT_DEESCALATION_WINDOW_MS) { + mMitigationCalls.removeFirst(); + } + } catch (NoSuchElementException ignore) { + } + + return mMitigationCalls.size(); + } + + /** + * Before writing to disk, make the mitigation call timestamps relative to the current + * system uptime. This is because they need to be relative to the uptime which will reset + * at the next boot. + * + * @return a LongArrayQueue of the mitigation calls relative to the current system uptime. + */ + @GuardedBy("sLock") + public LongArrayQueue normalizeMitigationCalls() { + LongArrayQueue normalized = new LongArrayQueue(); + final long now = mSystemClock.uptimeMillis(); + for (int i = 0; i < mMitigationCalls.size(); i++) { + normalized.addLast(mMitigationCalls.get(i) - now); + } + return normalized; + } + + /** + * Sets the initial health check duration. + * + * @return the new health check state + */ + @GuardedBy("sLock") + public int setHealthCheckActiveLocked(long initialHealthCheckDurationMs) { + if (initialHealthCheckDurationMs <= 0) { + Slog.wtf(TAG, "Cannot set non-positive health check duration " + + initialHealthCheckDurationMs + "ms for package " + getName() + + ". Using total duration " + mDurationMs + "ms instead"); + initialHealthCheckDurationMs = mDurationMs; + } + if (mHealthCheckState == HealthCheckState.INACTIVE) { + // Transitions to ACTIVE + mHealthCheckDurationMs = initialHealthCheckDurationMs; + } + return updateHealthCheckStateLocked(); + } + + /** + * Updates the monitoring durations of the package. + * + * @return the new health check state + */ + @GuardedBy("sLock") + public int handleElapsedTimeLocked(long elapsedMs) { + if (elapsedMs <= 0) { + Slog.w(TAG, "Cannot handle non-positive elapsed time for package " + getName()); + return mHealthCheckState; + } + // Transitions to FAILED if now <= 0 and health check not passed + mDurationMs -= elapsedMs; + if (mHealthCheckState == HealthCheckState.ACTIVE) { + // We only update health check durations if we have #setHealthCheckActiveLocked + // This ensures we don't leave the INACTIVE state for an unexpected elapsed time + // Transitions to FAILED if now <= 0 and health check not passed + mHealthCheckDurationMs -= elapsedMs; + } + return updateHealthCheckStateLocked(); + } + + /** Explicitly update the monitoring duration of the package. */ + @GuardedBy("sLock") + public void updateHealthCheckDuration(long newDurationMs) { + mDurationMs = newDurationMs; + } + + /** + * Marks the health check as passed and transitions to {@link HealthCheckState.PASSED} + * if not yet {@link HealthCheckState.FAILED}. + * + * @return the new {@link HealthCheckState health check state} + */ + @GuardedBy("sLock") + @HealthCheckState + public int tryPassHealthCheckLocked() { + if (mHealthCheckState != HealthCheckState.FAILED) { + // FAILED is a final state so only pass if we haven't failed + // Transition to PASSED + mHasPassedHealthCheck = true; + } + return updateHealthCheckStateLocked(); + } + + /** Returns the monitored package name. */ + private String getName() { + return mPackageName; + } + + /** + * Returns the current {@link HealthCheckState health check state}. + */ + @GuardedBy("sLock") + @HealthCheckState + public int getHealthCheckStateLocked() { + return mHealthCheckState; + } + + /** + * Returns the shortest duration before the package should be scheduled for a prune. + * + * @return the duration or {@link Long#MAX_VALUE} if the package should not be scheduled + */ + @GuardedBy("sLock") + public long getShortestScheduleDurationMsLocked() { + // Consider health check duration only if #isPendingHealthChecksLocked is true + return Math.min(toPositive(mDurationMs), + isPendingHealthChecksLocked() + ? toPositive(mHealthCheckDurationMs) : Long.MAX_VALUE); + } + + /** + * Returns {@code true} if the total duration left to monitor the package is less than or + * equal to 0 {@code false} otherwise. + */ + @GuardedBy("sLock") + public boolean isExpiredLocked() { + return mDurationMs <= 0; + } + + /** + * Returns {@code true} if the package, {@link #getName} is expecting health check results + * {@code false} otherwise. + */ + @GuardedBy("sLock") + public boolean isPendingHealthChecksLocked() { + return mHealthCheckState == HealthCheckState.ACTIVE + || mHealthCheckState == HealthCheckState.INACTIVE; + } + + /** + * Updates the health check state based on {@link #mHasPassedHealthCheck} + * and {@link #mHealthCheckDurationMs}. + * + * @return the new {@link HealthCheckState health check state} + */ + @GuardedBy("sLock") + @HealthCheckState + private int updateHealthCheckStateLocked() { + int oldState = mHealthCheckState; + if (mHasPassedHealthCheck) { + // Set final state first to avoid ambiguity + mHealthCheckState = HealthCheckState.PASSED; + } else if (mHealthCheckDurationMs <= 0 || mDurationMs <= 0) { + // Set final state first to avoid ambiguity + mHealthCheckState = HealthCheckState.FAILED; + } else if (mHealthCheckDurationMs == Long.MAX_VALUE) { + mHealthCheckState = HealthCheckState.INACTIVE; + } else { + mHealthCheckState = HealthCheckState.ACTIVE; + } + + if (oldState != mHealthCheckState) { + Slog.i(TAG, "Updated health check state for package " + getName() + ": " + + toString(oldState) + " -> " + toString(mHealthCheckState)); + } + return mHealthCheckState; + } + + /** Returns a {@link String} representation of the current health check state. */ + private String toString(@HealthCheckState int state) { + switch (state) { + case HealthCheckState.ACTIVE: + return "ACTIVE"; + case HealthCheckState.INACTIVE: + return "INACTIVE"; + case HealthCheckState.PASSED: + return "PASSED"; + case HealthCheckState.FAILED: + return "FAILED"; + default: + return "UNKNOWN"; + } + } + + /** Returns {@code value} if it is greater than 0 or {@link Long#MAX_VALUE} otherwise. */ + private long toPositive(long value) { + return value > 0 ? value : Long.MAX_VALUE; + } + + /** Compares the equality of this object with another {@link MonitoredPackage}. */ + @VisibleForTesting + boolean isEqualTo(MonitoredPackage pkg) { + return (getName().equals(pkg.getName())) + && mDurationMs == pkg.mDurationMs + && mHasPassedHealthCheck == pkg.mHasPassedHealthCheck + && mHealthCheckDurationMs == pkg.mHealthCheckDurationMs + && (mMitigationCalls.toString()).equals(pkg.mMitigationCalls.toString()); + } + } + + @GuardedBy("sLock") + @SuppressWarnings("GuardedBy") + void saveAllObserversBootMitigationCountToMetadata(String filePath) { + HashMap<String, Integer> bootMitigationCounts = new HashMap<>(); + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); + } + + FileOutputStream fileStream = null; + ObjectOutputStream objectStream = null; + try { + fileStream = new FileOutputStream(new File(filePath)); + objectStream = new ObjectOutputStream(fileStream); + objectStream.writeObject(bootMitigationCounts); + objectStream.flush(); + } catch (Exception e) { + Slog.i(TAG, "Could not save observers metadata to file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); + } + } + + /** + * Handles the thresholding logic for system server boots. + */ + class BootThreshold { + + private final int mBootTriggerCount; + private final long mTriggerWindow; + + BootThreshold(int bootTriggerCount, long triggerWindow) { + this.mBootTriggerCount = bootTriggerCount; + this.mTriggerWindow = triggerWindow; + } + + public void reset() { + setStart(0); + setCount(0); + } + + protected int getCount() { + return CrashRecoveryProperties.rescueBootCount().orElse(0); + } + + protected void setCount(int count) { + CrashRecoveryProperties.rescueBootCount(count); + } + + public long getStart() { + return CrashRecoveryProperties.rescueBootStart().orElse(0L); + } + + public int getMitigationCount() { + return CrashRecoveryProperties.bootMitigationCount().orElse(0); + } + + public void setStart(long start) { + CrashRecoveryProperties.rescueBootStart(getStartTime(start)); + } + + public void setMitigationStart(long start) { + CrashRecoveryProperties.bootMitigationStart(getStartTime(start)); + } + + public long getMitigationStart() { + return CrashRecoveryProperties.bootMitigationStart().orElse(0L); + } + + public void setMitigationCount(int count) { + CrashRecoveryProperties.bootMitigationCount(count); + } + + private static long constrain(long amount, long low, long high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public long getStartTime(long start) { + final long now = mSystemClock.uptimeMillis(); + return constrain(start, 0, now); + } + + public void saveMitigationCountToMetadata() { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(METADATA_FILE))) { + writer.write(String.valueOf(getMitigationCount())); + } catch (Exception e) { + Slog.e(TAG, "Could not save metadata to file: " + e); + } + } + + public void readMitigationCountFromMetadataIfNecessary() { + File bootPropsFile = new File(METADATA_FILE); + if (bootPropsFile.exists()) { + try (BufferedReader reader = new BufferedReader(new FileReader(METADATA_FILE))) { + String mitigationCount = reader.readLine(); + setMitigationCount(Integer.parseInt(mitigationCount)); + bootPropsFile.delete(); + } catch (Exception e) { + Slog.i(TAG, "Could not read metadata file: " + e); + } + } + } + + + /** Increments the boot counter, and returns whether the device is bootlooping. */ + @GuardedBy("sLock") + public boolean incrementAndTest() { + if (Flags.recoverabilityDetection()) { + readAllObserversBootMitigationCountIfNecessary(METADATA_FILE); + } else { + readMitigationCountFromMetadataIfNecessary(); + } + + final long now = mSystemClock.uptimeMillis(); + if (now - getStart() < 0) { + Slog.e(TAG, "Window was less than zero. Resetting start to current time."); + setStart(now); + setMitigationStart(now); + } + if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) { + setMitigationStart(now); + if (Flags.recoverabilityDetection()) { + resetAllObserversBootMitigationCount(); + } else { + setMitigationCount(0); + } + } + final long window = now - getStart(); + if (window >= mTriggerWindow) { + setCount(1); + setStart(now); + return false; + } else { + int count = getCount() + 1; + setCount(count); + EventLog.writeEvent(LOG_TAG_RESCUE_NOTE, Process.ROOT_UID, count, window); + if (Flags.recoverabilityDetection()) { + // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply + // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT. + return (count >= mBootTriggerCount) + || (performedMitigationsDuringWindow() && count > 1); + } + return count >= mBootTriggerCount; + } + } + + @GuardedBy("sLock") + private boolean performedMitigationsDuringWindow() { + for (ObserverInternal observerInternal: mAllObservers.values()) { + if (observerInternal.getBootMitigationCount() > 0) { + return true; + } + } + return false; + } + + @GuardedBy("sLock") + private void resetAllObserversBootMitigationCount() { + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + observer.setBootMitigationCount(0); + } + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + } + + @GuardedBy("sLock") + @SuppressWarnings("GuardedBy") + void readAllObserversBootMitigationCountIfNecessary(String filePath) { + File metadataFile = new File(filePath); + if (metadataFile.exists()) { + FileInputStream fileStream = null; + ObjectInputStream objectStream = null; + HashMap<String, Integer> bootMitigationCounts = null; + try { + fileStream = new FileInputStream(metadataFile); + objectStream = new ObjectInputStream(fileStream); + bootMitigationCounts = + (HashMap<String, Integer>) objectStream.readObject(); + } catch (Exception e) { + Slog.i(TAG, "Could not read observer metadata file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); + } + + if (bootMitigationCounts == null || bootMitigationCounts.isEmpty()) { + Slog.i(TAG, "No observer in metadata file"); + return; + } + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + if (bootMitigationCounts.containsKey(observer.name)) { + observer.setBootMitigationCount( + bootMitigationCounts.get(observer.name)); + } + } + } + } + } + + /** + * Register broadcast receiver for shutdown. + * We would save the observer state to persist across boots. + * + * @hide + */ + public void registerShutdownBroadcastReceiver() { + BroadcastReceiver shutdownEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Only write if intent is relevant to device reboot or shutdown. + String intentAction = intent.getAction(); + if (ACTION_REBOOT.equals(intentAction) + || ACTION_SHUTDOWN.equals(intentAction)) { + writeNow(); + } + } + }; + + // Setup receiver for device reboots or shutdowns. + IntentFilter filter = new IntentFilter(ACTION_REBOOT); + filter.addAction(ACTION_SHUTDOWN); + mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null, + /* run on main thread */ null); + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java new file mode 100644 index 000000000000..846da194b3c3 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java @@ -0,0 +1,861 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; +import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; +import android.os.Build; +import android.os.PowerManager; +import android.os.RecoverySystem; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import android.sysprop.CrashRecoveryProperties; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.FileUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.PackageWatchdog.FailureReasons; +import com.android.server.PackageWatchdog.PackageHealthObserver; +import com.android.server.PackageWatchdog.PackageHealthObserverImpact; +import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; + +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Utilities to help rescue the system from crash loops. Callers are expected to + * report boot events and persistent app crashes, and if they happen frequently + * enough this class will slowly escalate through several rescue operations + * before finally rebooting and prompting the user if they want to wipe data as + * a last resort. + * + * @hide + */ +public class RescueParty { + @VisibleForTesting + static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue"; + @VisibleForTesting + static final int LEVEL_NONE = 0; + @VisibleForTesting + static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1; + @VisibleForTesting + static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2; + @VisibleForTesting + static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3; + @VisibleForTesting + static final int LEVEL_WARM_REBOOT = 4; + @VisibleForTesting + static final int LEVEL_FACTORY_RESET = 5; + @VisibleForTesting + static final int RESCUE_LEVEL_NONE = 0; + @VisibleForTesting + static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1; + @VisibleForTesting + static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2; + @VisibleForTesting + static final int RESCUE_LEVEL_WARM_REBOOT = 3; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6; + @VisibleForTesting + static final int RESCUE_LEVEL_FACTORY_RESET = 7; + + @IntDef(prefix = { "RESCUE_LEVEL_" }, value = { + RESCUE_LEVEL_NONE, + RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_WARM_REBOOT, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES, + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS, + RESCUE_LEVEL_FACTORY_RESET + }) + @Retention(RetentionPolicy.SOURCE) + @interface RescueLevels {} + + @VisibleForTesting + static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit"; + @VisibleForTesting + static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1; + @VisibleForTesting + static final String TAG = "RescueParty"; + @VisibleForTesting + static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); + @VisibleForTesting + static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS; + // The DeviceConfig namespace containing all RescueParty switches. + @VisibleForTesting + static final String NAMESPACE_CONFIGURATION = "configuration"; + @VisibleForTesting + static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG = + "namespace_to_package_mapping"; + @VisibleForTesting + static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440; + + private static final String NAME = "rescue-party-observer"; + + private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue"; + private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device"; + private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = + "persist.device_config.configuration.disable_rescue_party"; + private static final String PROP_DISABLE_FACTORY_RESET_FLAG = + "persist.device_config.configuration.disable_rescue_party_factory_reset"; + private static final String PROP_THROTTLE_DURATION_MIN_FLAG = + "persist.device_config.configuration.rescue_party_throttle_duration_min"; + + private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT + | ApplicationInfo.FLAG_SYSTEM; + + /** + * EventLog tags used when logging into the event log. Note the values must be sync with + * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct + * name translation. + */ + private static final int LOG_TAG_RESCUE_SUCCESS = 2902; + private static final int LOG_TAG_RESCUE_FAILURE = 2903; + + /** Register the Rescue Party observer as a Package Watchdog health observer */ + public static void registerHealthObserver(Context context) { + PackageWatchdog.getInstance(context).registerHealthObserver( + context.getMainExecutor(), RescuePartyObserver.getInstance(context)); + } + + private static boolean isDisabled() { + // Check if we're explicitly enabled for testing + if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) { + return false; + } + + // We're disabled if the DeviceConfig disable flag is set to true. + // This is in case that an emergency rollback of the feature is needed. + if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) { + Slog.v(TAG, "Disabled because of DeviceConfig flag"); + return true; + } + + // We're disabled on all engineering devices + if (Build.TYPE.equals("eng")) { + Slog.v(TAG, "Disabled because of eng build"); + return true; + } + + // We're disabled on userdebug devices connected over USB, since that's + // a decent signal that someone is actively trying to debug the device, + // or that it's in a lab environment. + if (Build.TYPE.equals("userdebug") && isUsbActive()) { + Slog.v(TAG, "Disabled because of active USB connection"); + return true; + } + + // One last-ditch check + if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) { + Slog.v(TAG, "Disabled because of manual property"); + return true; + } + + return false; + } + + /** + * Check if we're currently attempting to reboot for a factory reset. This method must + * return true if RescueParty tries to reboot early during a boot loop, since the device + * will not be fully booted at this time. + */ + public static boolean isRecoveryTriggeredReboot() { + return isFactoryResetPropertySet() || isRebootPropertySet(); + } + + static boolean isFactoryResetPropertySet() { + return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); + } + + static boolean isRebootPropertySet() { + return CrashRecoveryProperties.attemptingReboot().orElse(false); + } + + protected static long getLastFactoryResetTimeMs() { + return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L); + } + + protected static int getMaxRescueLevelAttempted() { + return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(LEVEL_NONE); + } + + protected static void setFactoryResetProperty(boolean value) { + CrashRecoveryProperties.attemptingFactoryReset(value); + } + protected static void setRebootProperty(boolean value) { + CrashRecoveryProperties.attemptingReboot(value); + } + + protected static void setLastFactoryResetTimeMs(long value) { + CrashRecoveryProperties.lastFactoryResetTimeMs(value); + } + + protected static void setMaxRescueLevelAttempted(int level) { + CrashRecoveryProperties.maxRescueLevelAttempted(level); + } + + @VisibleForTesting + static long getElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + private static int getMaxRescueLevel(boolean mayPerformReboot) { + if (Flags.recoverabilityDetection()) { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT, + DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT); + } + return RESCUE_LEVEL_FACTORY_RESET; + } else { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + } + return LEVEL_FACTORY_RESET; + } + } + + private static int getMaxRescueLevel() { + if (!SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return Level.factoryReset(); + } + return Level.reboot(); + } + + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * + * @param mitigationCount: the mitigation attempt number (1 = first attempt etc.) + * @param mayPerformReboot: whether or not a reboot and factory reset may be performed + * for the given failure. + * @return the rescue level for the n-th mitigation attempt. + */ + private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + if (mitigationCount == 1) { + return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS; + } else if (mitigationCount == 2) { + return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES; + } else if (mitigationCount == 3) { + return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + } else if (mitigationCount == 4) { + return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT); + } else if (mitigationCount >= 5) { + return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET); + } else { + Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount); + return LEVEL_NONE; + } + } else { + if (mitigationCount == 1) { + return Level.reboot(); + } else if (mitigationCount >= 2) { + return Math.min(getMaxRescueLevel(), Level.factoryReset()); + } else { + Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount); + return LEVEL_NONE; + } + } + } + + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and + * all device config reset). Behaves as if one mitigation attempt was already done. + * + * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). + * @param mayPerformReboot whether or not a reboot and factory reset may be performed + * for the given failure. + * @param failedPackage in case of bootloop this is null. + * @return the rescue level for the n-th mitigation attempt. + */ + private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot, + @Nullable VersionedPackage failedPackage) { + // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed + // package. + if (failedPackage == null && mitigationCount > 0) { + mitigationCount += 1; + } + if (mitigationCount == 1) { + return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 2) { + return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 3) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT); + } else if (mitigationCount == 4) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS); + } else if (mitigationCount == 5) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES); + } else if (mitigationCount == 6) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS); + } else if (mitigationCount >= 7) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET); + } else { + return RESCUE_LEVEL_NONE; + } + } + + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * + * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). + * @return the rescue level for the n-th mitigation attempt. + */ + private static @RescueLevels int getRescueLevel(int mitigationCount) { + if (mitigationCount == 1) { + return Level.reboot(); + } else if (mitigationCount >= 2) { + return Math.min(getMaxRescueLevel(), Level.factoryReset()); + } else { + return Level.none(); + } + } + + private static void executeRescueLevel(Context context, @Nullable String failedPackage, + int level) { + Slog.w(TAG, "Attempting rescue level " + levelToString(level)); + try { + executeRescueLevelInternal(context, level, failedPackage); + EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level); + String successMsg = "Finished rescue level " + levelToString(level); + if (!TextUtils.isEmpty(failedPackage)) { + successMsg += " for package " + failedPackage; + } + logCrashRecoveryEvent(Log.DEBUG, successMsg); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } + + private static void executeRescueLevelInternal(Context context, int level, @Nullable + String failedPackage) throws Exception { + if (Flags.recoverabilityDetection()) { + executeRescueLevelInternalNew(context, level, failedPackage); + } else { + executeRescueLevelInternalOld(context, level, failedPackage); + } + } + + private static void executeRescueLevelInternalOld(Context context, int level, @Nullable + String failedPackage) throws Exception { + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, + level, levelToString(level)); + // Try our best to reset all settings possible, and once finished + // rethrow any exception that we encountered + Exception res = null; + switch (level) { + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + break; + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + break; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + break; + case LEVEL_WARM_REBOOT: + executeWarmReboot(context, level, failedPackage); + break; + case LEVEL_FACTORY_RESET: + // Before the completion of Reboot, if any crash happens then PackageWatchdog + // escalates to next level i.e. factory reset, as they happen in separate threads. + // Adding a check to prevent factory reset to execute before above reboot completes. + // Note: this reboot property is not persistent resets after reboot is completed. + if (isRebootPropertySet()) { + return; + } + executeFactoryReset(context, level, failedPackage); + break; + } + + if (res != null) { + throw res; + } + } + + private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level, + @Nullable String failedPackage) throws Exception { + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, + level, levelToString(level)); + switch (level) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + break; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + break; + case RESCUE_LEVEL_WARM_REBOOT: + executeWarmReboot(context, level, failedPackage); + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + // do nothing + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + // do nothing + break; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + // do nothing + break; + case RESCUE_LEVEL_FACTORY_RESET: + // Before the completion of Reboot, if any crash happens then PackageWatchdog + // escalates to next level i.e. factory reset, as they happen in separate threads. + // Adding a check to prevent factory reset to execute before above reboot completes. + // Note: this reboot property is not persistent resets after reboot is completed. + if (isRebootPropertySet()) { + return; + } + executeFactoryReset(context, level, failedPackage); + break; + } + } + + private static void executeWarmReboot(Context context, int level, + @Nullable String failedPackage) { + if (Flags.deprecateFlagsAndSettingsResets()) { + if (shouldThrottleReboot()) { + return; + } + } + + // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog + // when device shutting down. + setRebootProperty(true); + + if (Flags.synchronousRebootInRescueParty()) { + try { + PowerManager pm = context.getSystemService(PowerManager.class); + if (pm != null) { + pm.reboot(TAG); + } + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } else { + Runnable runnable = () -> { + try { + PowerManager pm = context.getSystemService(PowerManager.class); + if (pm != null) { + pm.reboot(TAG); + } + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + } + + private static void executeFactoryReset(Context context, int level, + @Nullable String failedPackage) { + if (Flags.deprecateFlagsAndSettingsResets()) { + if (shouldThrottleReboot()) { + return; + } + } + setFactoryResetProperty(true); + long now = System.currentTimeMillis(); + setLastFactoryResetTimeMs(now); + + if (Flags.synchronousRebootInRescueParty()) { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } else { + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, + TAG + "," + failedPackage); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + } + + + private static String getCompleteMessage(Throwable t) { + final StringBuilder builder = new StringBuilder(); + builder.append(t.getMessage()); + while ((t = t.getCause()) != null) { + builder.append(": ").append(t.getMessage()); + } + return builder.toString(); + } + + private static void logRescueException(int level, @Nullable String failedPackageName, + Throwable t) { + final String msg = getCompleteMessage(t); + EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg); + String failureMsg = "Failed rescue level " + levelToString(level); + if (!TextUtils.isEmpty(failedPackageName)) { + failureMsg += " for package " + failedPackageName; + } + logCrashRecoveryEvent(Log.ERROR, failureMsg + ": " + msg); + } + + private static int mapRescueLevelToUserImpact(int rescueLevel) { + if (Flags.recoverabilityDetection()) { + switch (rescueLevel) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_40; + case RESCUE_LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80; + case RESCUE_LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + } else { + switch (rescueLevel) { + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + case LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + } + } + + /** + * Handle mitigation action for package failures. This observer will be register to Package + * Watchdog and will receive calls about package failures. This observer is persistent so it + * may choose to mitigate failures for packages it has not explicitly asked to observe. + */ + public static class RescuePartyObserver implements PackageHealthObserver { + + private final Context mContext; + private final Map<String, Set<String>> mCallingPackageNamespaceSetMap = new HashMap<>(); + private final Map<String, Set<String>> mNamespaceCallingPackageSetMap = new HashMap<>(); + + @GuardedBy("RescuePartyObserver.class") + static RescuePartyObserver sRescuePartyObserver; + + private RescuePartyObserver(Context context) { + mContext = context; + } + + /** Creates or gets singleton instance of RescueParty. */ + public static RescuePartyObserver getInstance(Context context) { + synchronized (RescuePartyObserver.class) { + if (sRescuePartyObserver == null) { + sRescuePartyObserver = new RescuePartyObserver(context); + } + return sRescuePartyObserver; + } + } + + /** Gets singleton instance. It returns null if the instance is not created yet.*/ + @Nullable + public static RescuePartyObserver getInstanceIfCreated() { + synchronized (RescuePartyObserver.class) { + return sRescuePartyObserver; + } + } + + @VisibleForTesting + static void reset() { + synchronized (RescuePartyObserver.class) { + sRescuePartyObserver = null; + } + } + + @Override + public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, + @FailureReasons int failureReason, int mitigationCount) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH + || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) { + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage), failedPackage)); + } else { + impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); + } + } else { + impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage))); + } + } + + Slog.i(TAG, "Checking available remediations for health check failure." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " failureReason: " + failureReason + + " available impact: " + impact); + return impact; + } + + @Override + public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, + @FailureReasons int failureReason, int mitigationCount) { + if (isDisabled()) { + return MITIGATION_RESULT_SKIPPED; + } + Slog.i(TAG, "Executing remediation." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " failureReason: " + failureReason + + " mitigationCount: " + mitigationCount); + if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH + || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) { + final int level; + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage), + failedPackage); + } else { + level = getRescueLevel(mitigationCount); + } + } else { + level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage)); + } + executeRescueLevel(mContext, + failedPackage == null ? null : failedPackage.getPackageName(), level); + return MITIGATION_RESULT_SUCCESS; + } else { + return MITIGATION_RESULT_SKIPPED; + } + } + + @Override + public boolean isPersistent() { + return true; + } + + @Override + public boolean mayObservePackage(String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + // A package is a module if this is non-null + if (pm.getModuleInfo(packageName, 0) != null) { + return true; + } + } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) { + } + + return isPersistentSystemApp(packageName); + } + + @Override + public int onBootLoop(int mitigationCount) { + if (isDisabled()) { + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + true, /*failedPackage=*/ null)); + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount)); + } + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); + } + } + + @Override + public int onExecuteBootLoopMitigation(int mitigationCount) { + if (isDisabled()) { + return MITIGATION_RESULT_SKIPPED; + } + boolean mayPerformReboot = !shouldThrottleReboot(); + final int level; + if (Flags.recoverabilityDetection()) { + if (!Flags.deprecateFlagsAndSettingsResets()) { + level = getRescueLevel(mitigationCount, mayPerformReboot, + /*failedPackage=*/ null); + } else { + level = getRescueLevel(mitigationCount); + } + } else { + level = getRescueLevel(mitigationCount, mayPerformReboot); + } + executeRescueLevel(mContext, /*failedPackage=*/ null, level); + return MITIGATION_RESULT_SUCCESS; + } + + @Override + public String getUniqueIdentifier() { + return NAME; + } + + /** + * Returns {@code true} if the failing package is non-null and performing a reboot or + * prompting a factory reset is an acceptable mitigation strategy for the package's + * failure, {@code false} otherwise. + */ + private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) { + if (failingPackage == null) { + return false; + } + if (shouldThrottleReboot()) { + return false; + } + + return isPersistentSystemApp(failingPackage.getPackageName()); + } + + private boolean isPersistentSystemApp(@NonNull String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private synchronized Set<String> getCallingPackagesSet(String namespace) { + return mNamespaceCallingPackageSetMap.get(namespace); + } + } + + /** + * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset. + * Will return {@code false} if a factory reset was already offered recently. + */ + private static boolean shouldThrottleReboot() { + Long lastResetTime = getLastFactoryResetTimeMs(); + long now = System.currentTimeMillis(); + long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG, + DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN); + return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin); + } + + /** + * Hacky test to check if the device has an active USB connection, which is + * a good proxy for someone doing local development work. + */ + private static boolean isUsbActive() { + if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) { + Slog.v(TAG, "Assuming virtual device is connected over USB"); + return true; + } + try { + final String state = FileUtils + .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, ""); + return "CONFIGURED".equals(state.trim()); + } catch (Throwable t) { + Slog.w(TAG, "Failed to determine if device was on USB", t); + return false; + } + } + + private static class Level { + static int none() { + return Flags.recoverabilityDetection() ? RESCUE_LEVEL_NONE : LEVEL_NONE; + } + + static int reboot() { + return Flags.recoverabilityDetection() ? RESCUE_LEVEL_WARM_REBOOT : LEVEL_WARM_REBOOT; + } + + static int factoryReset() { + return Flags.recoverabilityDetection() + ? RESCUE_LEVEL_FACTORY_RESET + : LEVEL_FACTORY_RESET; + } + } + + private static String levelToString(int level) { + if (Flags.recoverabilityDetection()) { + switch (level) { + case RESCUE_LEVEL_NONE: + return "NONE"; + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return "SCOPED_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return "ALL_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case RESCUE_LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } + } else { + switch (level) { + case LEVEL_NONE: + return "NONE"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java new file mode 100644 index 000000000000..8a81aaa1e636 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.crashrecovery; + +import android.content.Context; + +import com.android.server.PackageWatchdog; +import com.android.server.RescueParty; +import com.android.server.SystemService; + + +/** This class encapsulate the lifecycle methods of CrashRecovery module. + * + * @hide + */ +public class CrashRecoveryModule { + private static final String TAG = "CrashRecoveryModule"; + + /** Lifecycle definition for CrashRecovery module. */ + public static class Lifecycle extends SystemService { + private Context mSystemContext; + private PackageWatchdog mPackageWatchdog; + + public Lifecycle(Context context) { + super(context); + mSystemContext = context; + mPackageWatchdog = PackageWatchdog.getInstance(context); + } + + @Override + public void onStart() { + RescueParty.registerHealthObserver(mSystemContext); + mPackageWatchdog.registerShutdownBroadcastReceiver(); + mPackageWatchdog.noteBoot(); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + mPackageWatchdog.onPackagesReady(); + } + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java new file mode 100644 index 000000000000..2e2a93776f9d --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.crashrecovery; + +import android.os.Environment; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * Class containing helper methods for the CrashRecoveryModule. + * + * @hide + */ +public class CrashRecoveryUtils { + private static final String TAG = "CrashRecoveryUtils"; + private static final long MAX_CRITICAL_INFO_DUMP_SIZE = 1000 * 1000; // ~1MB + private static final Object sFileLock = new Object(); + + /** Persist recovery related events in crashrecovery events file.**/ + public static void logCrashRecoveryEvent(int priority, String msg) { + Log.println(priority, TAG, msg); + try { + File fname = getCrashRecoveryEventsFile(); + synchronized (sFileLock) { + FileOutputStream out = new FileOutputStream(fname, true); + PrintWriter pw = new PrintWriter(out); + String dateString = LocalDateTime.now(ZoneId.systemDefault()).toString(); + pw.println(dateString + ": " + msg); + pw.close(); + } + } catch (IOException e) { + Log.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage()); + } + } + + /** Dump recovery related events from crashrecovery events file.**/ + public static void dumpCrashRecoveryEvents(IndentingPrintWriter pw) { + pw.println("CrashRecovery Events: "); + pw.increaseIndent(); + final File file = getCrashRecoveryEventsFile(); + final long skipSize = file.length() - MAX_CRITICAL_INFO_DUMP_SIZE; + synchronized (sFileLock) { + try (BufferedReader in = new BufferedReader(new FileReader(file))) { + if (skipSize > 0) { + in.skip(skipSize); + } + String line; + while ((line = in.readLine()) != null) { + pw.println(line); + } + } catch (IOException e) { + Log.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage()); + } + } + pw.decreaseIndent(); + } + + private static File getCrashRecoveryEventsFile() { + File systemDir = new File(Environment.getDataDirectory(), "system"); + return new File(systemDir, "crashrecovery-events.txt"); + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java new file mode 100644 index 000000000000..4978df491c62 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -0,0 +1,785 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.rollback; + +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED; +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; +import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; + +import android.annotation.AnyThread; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.WorkerThread; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.crashrecovery.flags.Flags; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.PowerManager; +import android.os.SystemProperties; +import android.sysprop.CrashRecoveryProperties; +import android.util.ArraySet; +import android.util.FileUtils; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.server.PackageWatchdog; +import com.android.server.PackageWatchdog.FailureReasons; +import com.android.server.PackageWatchdog.PackageHealthObserver; +import com.android.server.PackageWatchdog.PackageHealthObserverImpact; +import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * {@link PackageHealthObserver} for {@link RollbackManagerService}. + * This class monitors crashes and triggers RollbackManager rollback accordingly. + * It also monitors native crashes for some short while after boot. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) +@SuppressLint({"CallbackName"}) +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public final class RollbackPackageHealthObserver implements PackageHealthObserver { + private static final String TAG = "RollbackPackageHealthObserver"; + private static final String NAME = "rollback-observer"; + private static final String CLASS_NAME = RollbackPackageHealthObserver.class.getName(); + + private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT + | ApplicationInfo.FLAG_SYSTEM; + + private static final String PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG = + "persist.device_config.configuration.disable_high_impact_rollback"; + + private final Context mContext; + private final Handler mHandler; + private final File mLastStagedRollbackIdsFile; + private final File mTwoPhaseRollbackEnabledFile; + // Staged rollback ids that have been committed but their session is not yet ready + private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>(); + // True if needing to roll back only rebootless apexes when native crash happens + private boolean mTwoPhaseRollbackEnabled; + + @VisibleForTesting + public RollbackPackageHealthObserver(@NonNull Context context) { + mContext = context; + HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); + File dataDir = new File(Environment.getDataDirectory(), "rollback-observer"); + dataDir.mkdirs(); + mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids"); + mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled"); + PackageWatchdog.getInstance(mContext).registerHealthObserver(context.getMainExecutor(), + this); + + if (SystemProperties.getBoolean("sys.boot_completed", false)) { + // Load the value from the file if system server has crashed and restarted + mTwoPhaseRollbackEnabled = readBoolean(mTwoPhaseRollbackEnabledFile); + } else { + // Disable two-phase rollback for a normal reboot. We assume the rebootless apex + // installed before reboot is stable if native crash didn't happen. + mTwoPhaseRollbackEnabled = false; + writeBoolean(mTwoPhaseRollbackEnabledFile, false); + } + } + + @Override + public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, + @FailureReasons int failureReason, int mitigationCount) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( + availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW); + if (!lowImpactRollbacks.isEmpty()) { + if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + // For native crashes, we will directly roll back any available rollbacks at low + // impact level + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else if (getRollbackForPackage(failedPackage, lowImpactRollbacks) != null) { + // Rollback is available for crashing low impact package + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else { + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; + } + } + } else { + boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class) + .getAvailableRollbacks().isEmpty(); + + if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH + && anyRollbackAvailable) { + // For native crashes, we will directly roll back any available rollbacks + // Note: For non-native crashes the rollback-all step has higher impact + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else if (getAvailableRollback(failedPackage) != null) { + // Rollback is available, we may get a callback into #onExecuteHealthCheckMitigation + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; + } else if (anyRollbackAvailable) { + // If any rollbacks are available, we will commit them + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; + } + } + + Slog.i(TAG, "Checking available remediations for health check failure." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " failureReason: " + failureReason + + " available impact: " + impact); + return impact; + } + + @Override + public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, + @FailureReasons int rollbackReason, int mitigationCount) { + Slog.i(TAG, "Executing remediation." + + " failedPackage: " + + (failedPackage == null ? null : failedPackage.getPackageName()) + + " rollbackReason: " + rollbackReason + + " mitigationCount: " + mitigationCount); + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); + return MITIGATION_RESULT_SUCCESS; + } + + List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( + availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW); + RollbackInfo rollback = getRollbackForPackage(failedPackage, lowImpactRollbacks); + if (rollback != null) { + mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); + } else if (!lowImpactRollbacks.isEmpty()) { + // Apply all available low impact rollbacks. + mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); + } + } else { + if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + mHandler.post(() -> rollbackAll(rollbackReason)); + return MITIGATION_RESULT_SUCCESS; + } + + RollbackInfo rollback = getAvailableRollback(failedPackage); + if (rollback != null) { + mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); + } else { + mHandler.post(() -> rollbackAll(rollbackReason)); + } + } + + // Assume rollbacks executed successfully + return MITIGATION_RESULT_SUCCESS; + } + + @Override + public int onBootLoop(int mitigationCount) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + if (!availableRollbacks.isEmpty()) { + impact = getUserImpactBasedOnRollbackImpactLevel(availableRollbacks); + } + } + return impact; + } + + @Override + public int onExecuteBootLoopMitigation(int mitigationCount) { + if (Flags.recoverabilityDetection()) { + List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); + + triggerLeastImpactLevelRollback(availableRollbacks, + PackageWatchdog.FAILURE_REASON_BOOT_LOOP); + return MITIGATION_RESULT_SUCCESS; + } + return MITIGATION_RESULT_SKIPPED; + } + + @Override + @NonNull + public String getUniqueIdentifier() { + return NAME; + } + + @Override + public boolean isPersistent() { + return true; + } + + @Override + public boolean mayObservePackage(@NonNull String packageName) { + if (getAvailableRollbacks().isEmpty()) { + return false; + } + return isPersistentSystemApp(packageName); + } + + private List<RollbackInfo> getAvailableRollbacks() { + return mContext.getSystemService(RollbackManager.class).getAvailableRollbacks(); + } + + private boolean isPersistentSystemApp(@NonNull String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + ApplicationInfo info = pm.getApplicationInfo(packageName, 0); + return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private void assertInWorkerThread() { + Preconditions.checkState(mHandler.getLooper().isCurrentThread()); + } + + @AnyThread + @NonNull + public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) { + mHandler.post(() -> { + // Enable two-phase rollback when a rebootless apex rollback is made available. + // We assume the rebootless apex is stable and is less likely to be the cause + // if native crash doesn't happen before reboot. So we will clear the flag and disable + // two-phase rollback after reboot. + if (isRebootlessApex(rollback)) { + mTwoPhaseRollbackEnabled = true; + writeBoolean(mTwoPhaseRollbackEnabledFile, true); + } + }); + } + + private static boolean isRebootlessApex(RollbackInfo rollback) { + if (!rollback.isStaged()) { + for (PackageRollbackInfo info : rollback.getPackages()) { + if (info.isApex()) { + return true; + } + } + } + return false; + } + + /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot + * to check for native crashes and mitigate them if needed. + */ + @AnyThread + public void onBootCompletedAsync() { + mHandler.post(()->onBootCompleted()); + } + + @WorkerThread + private void onBootCompleted() { + assertInWorkerThread(); + + RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + if (!rollbackManager.getAvailableRollbacks().isEmpty()) { + // TODO(gavincorkery): Call into Package Watchdog from outside the observer + PackageWatchdog.getInstance(mContext).scheduleCheckAndMitigateNativeCrashes(); + } + + SparseArray<String> rollbackIds = popLastStagedRollbackIds(); + for (int i = 0; i < rollbackIds.size(); i++) { + WatchdogRollbackLogger.logRollbackStatusOnBoot(mContext, + rollbackIds.keyAt(i), rollbackIds.valueAt(i), + rollbackManager.getRecentlyCommittedRollbacks()); + } + } + + @AnyThread + private RollbackInfo getAvailableRollback(VersionedPackage failedPackage) { + RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) { + for (PackageRollbackInfo packageRollback : rollback.getPackages()) { + if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { + return rollback; + } + // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have + // to rely on complicated reasoning as below + + // Due to b/147666157, for apk in apex, we do not know the version we are rolling + // back from. But if a package X is embedded in apex A exclusively (not embedded in + // any other apex), which is not guaranteed, then it is sufficient to check only + // package names here, as the version of failedPackage and the PackageRollbackInfo + // can't be different. If failedPackage has a higher version, then it must have + // been updated somehow. There are two ways: it was updated by an update of apex A + // or updated directly as apk. In both cases, this rollback would have gotten + // expired when onPackageReplaced() was called. Since the rollback exists, it has + // same version as failedPackage. + if (packageRollback.isApkInApex() + && packageRollback.getVersionRolledBackFrom().getPackageName() + .equals(failedPackage.getPackageName())) { + return rollback; + } + } + } + return null; + } + + @AnyThread + private RollbackInfo getRollbackForPackage(@Nullable VersionedPackage failedPackage, + List<RollbackInfo> availableRollbacks) { + if (failedPackage == null) { + return null; + } + + for (RollbackInfo rollback : availableRollbacks) { + for (PackageRollbackInfo packageRollback : rollback.getPackages()) { + if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { + return rollback; + } + // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have + // to rely on complicated reasoning as below + + // Due to b/147666157, for apk in apex, we do not know the version we are rolling + // back from. But if a package X is embedded in apex A exclusively (not embedded in + // any other apex), which is not guaranteed, then it is sufficient to check only + // package names here, as the version of failedPackage and the PackageRollbackInfo + // can't be different. If failedPackage has a higher version, then it must have + // been updated somehow. There are two ways: it was updated by an update of apex A + // or updated directly as apk. In both cases, this rollback would have gotten + // expired when onPackageReplaced() was called. Since the rollback exists, it has + // same version as failedPackage. + if (packageRollback.isApkInApex() + && packageRollback.getVersionRolledBackFrom().getPackageName() + .equals(failedPackage.getPackageName())) { + return rollback; + } + } + } + return null; + } + + /** + * Returns {@code true} if staged session associated with {@code rollbackId} was marked + * as handled, {@code false} if already handled. + */ + @WorkerThread + private boolean markStagedSessionHandled(int rollbackId) { + assertInWorkerThread(); + return mPendingStagedRollbackIds.remove(rollbackId); + } + + /** + * Returns {@code true} if all pending staged rollback sessions were marked as handled, + * {@code false} if there is any left. + */ + @WorkerThread + private boolean isPendingStagedSessionsEmpty() { + assertInWorkerThread(); + return mPendingStagedRollbackIds.isEmpty(); + } + + private static boolean readBoolean(File file) { + try (FileInputStream fis = new FileInputStream(file)) { + return fis.read() == 1; + } catch (IOException ignore) { + return false; + } + } + + private static void writeBoolean(File file, boolean value) { + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(value ? 1 : 0); + fos.flush(); + FileUtils.sync(fos); + } catch (IOException ignore) { + } + } + + @WorkerThread + private void saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage) { + assertInWorkerThread(); + writeStagedRollbackId(mLastStagedRollbackIdsFile, stagedRollbackId, logPackage); + } + + static void writeStagedRollbackId(File file, int stagedRollbackId, + @Nullable VersionedPackage logPackage) { + try { + FileOutputStream fos = new FileOutputStream(file, true); + PrintWriter pw = new PrintWriter(fos); + String logPackageName = logPackage != null ? logPackage.getPackageName() : ""; + pw.append(String.valueOf(stagedRollbackId)).append(",").append(logPackageName); + pw.println(); + pw.flush(); + FileUtils.sync(fos); + pw.close(); + } catch (IOException e) { + Slog.e(TAG, "Failed to save last staged rollback id", e); + file.delete(); + } + } + + @WorkerThread + private SparseArray<String> popLastStagedRollbackIds() { + assertInWorkerThread(); + try { + return readStagedRollbackIds(mLastStagedRollbackIdsFile); + } finally { + mLastStagedRollbackIdsFile.delete(); + } + } + + static SparseArray<String> readStagedRollbackIds(File file) { + SparseArray<String> result = new SparseArray<>(); + try { + String line; + BufferedReader reader = new BufferedReader(new FileReader(file)); + while ((line = reader.readLine()) != null) { + // Each line is of the format: "id,logging_package" + String[] values = line.trim().split(","); + String rollbackId = values[0]; + String logPackageName = ""; + if (values.length > 1) { + logPackageName = values[1]; + } + result.put(Integer.parseInt(rollbackId), logPackageName); + } + } catch (Exception ignore) { + return new SparseArray<>(); + } + return result; + } + + + /** + * Returns true if the package name is the name of a module. + */ + @AnyThread + private boolean isModule(String packageName) { + // Check if the package is listed among the system modules or is an + // APK inside an updatable APEX. + try { + PackageManager pm = mContext.getPackageManager(); + final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */); + String apexPackageName = pkg.getApexPackageName(); + if (apexPackageName != null) { + packageName = apexPackageName; + } + + return pm.getModuleInfo(packageName, 0 /* flags */) != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * Rolls back the session that owns {@code failedPackage} + * + * @param rollback {@code rollbackInfo} of the {@code failedPackage} + * @param failedPackage the package that needs to be rolled back + */ + @WorkerThread + private void rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage, + @FailureReasons int rollbackReason) { + assertInWorkerThread(); + String failedPackageName = (failedPackage == null ? null : failedPackage.getPackageName()); + + Slog.i(TAG, "Rolling back package. RollbackId: " + rollback.getRollbackId() + + " failedPackage: " + failedPackageName + + " rollbackReason: " + rollbackReason); + logCrashRecoveryEvent(Log.DEBUG, String.format("Rolling back %s. Reason: %s", + failedPackageName, rollbackReason)); + final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason); + final String failedPackageToLog; + if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + failedPackageToLog = SystemProperties.get( + "sys.init.updatable_crashing_process_name", ""); + } else { + failedPackageToLog = failedPackage.getPackageName(); + } + VersionedPackage logPackageTemp = null; + if (isModule(failedPackage.getPackageName())) { + logPackageTemp = WatchdogRollbackLogger.getLogPackage(mContext, failedPackage); + } + + final VersionedPackage logPackage = logPackageTemp; + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE, + reasonToLog, failedPackageToLog); + + Consumer<Intent> onResult = result -> { + assertInWorkerThread(); + int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, + RollbackManager.STATUS_FAILURE); + if (status == RollbackManager.STATUS_SUCCESS) { + if (rollback.isStaged()) { + int rollbackId = rollback.getRollbackId(); + saveStagedRollbackId(rollbackId, logPackage); + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog + .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED, + reasonToLog, failedPackageToLog); + + } else { + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog + .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, + reasonToLog, failedPackageToLog); + } + } else { + WatchdogRollbackLogger.logEvent(logPackage, + CrashRecoveryStatsLog + .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, + reasonToLog, failedPackageToLog); + } + if (rollback.isStaged()) { + markStagedSessionHandled(rollback.getRollbackId()); + // Wait for all pending staged sessions to get handled before rebooting. + if (isPendingStagedSessionsEmpty()) { + CrashRecoveryProperties.attemptingReboot(true); + mContext.getSystemService(PowerManager.class).reboot("Rollback staged install"); + } + } + }; + + // Define a BroadcastReceiver to handle the result + BroadcastReceiver rollbackReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent result) { + mHandler.post(() -> onResult.accept(result)); + } + }; + + String intentActionName = CLASS_NAME + rollback.getRollbackId(); + // Register the BroadcastReceiver + mContext.registerReceiver(rollbackReceiver, + new IntentFilter(intentActionName), + Context.RECEIVER_NOT_EXPORTED); + + Intent intentReceiver = new Intent(intentActionName); + intentReceiver.putExtra("rollbackId", rollback.getRollbackId()); + intentReceiver.setPackage(mContext.getPackageName()); + intentReceiver.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + + PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext, + rollback.getRollbackId(), + intentReceiver, + PendingIntent.FLAG_MUTABLE); + + rollbackManager.commitRollback(rollback.getRollbackId(), + Collections.singletonList(failedPackage), + rollbackPendingIntent.getIntentSender()); + } + + /** + * Two-phase rollback: + * 1. roll back rebootless apexes first + * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done + * + * This approach gives us a better chance to correctly attribute native crash to rebootless + * apex update without rolling back Mainline updates which might contains critical security + * fixes. + */ + @WorkerThread + private boolean useTwoPhaseRollback(List<RollbackInfo> rollbacks) { + assertInWorkerThread(); + if (!mTwoPhaseRollbackEnabled) { + return false; + } + + Slog.i(TAG, "Rolling back all rebootless APEX rollbacks"); + boolean found = false; + for (RollbackInfo rollback : rollbacks) { + if (isRebootlessApex(rollback)) { + VersionedPackage firstRollback = + rollback.getPackages().get(0).getVersionRolledBackFrom(); + rollbackPackage(rollback, firstRollback, + PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); + found = true; + } + } + return found; + } + + /** + * Rollback the package that has minimum rollback impact level. + * @param availableRollbacks all available rollbacks + * @param rollbackReason reason to rollback + */ + private void triggerLeastImpactLevelRollback(List<RollbackInfo> availableRollbacks, + @FailureReasons int rollbackReason) { + int minRollbackImpactLevel = getMinRollbackImpactLevel(availableRollbacks); + + if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_LOW) { + // Apply all available low impact rollbacks. + mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason)); + } else if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH) { + // Check disable_high_impact_rollback device config before performing rollback + if (SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) { + return; + } + // Rollback one package at a time. If that doesn't resolve the issue, rollback + // next with same impact level. + mHandler.post(() -> rollbackHighImpact(availableRollbacks, rollbackReason)); + } + } + + /** + * sort the available high impact rollbacks by first package name to have a deterministic order. + * Apply the first available rollback. + * @param availableRollbacks all available rollbacks + * @param rollbackReason reason to rollback + */ + @WorkerThread + private void rollbackHighImpact(List<RollbackInfo> availableRollbacks, + @FailureReasons int rollbackReason) { + assertInWorkerThread(); + List<RollbackInfo> highImpactRollbacks = + getRollbacksAvailableForImpactLevel( + availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_HIGH); + + // sort rollbacks based on package name of the first package. This is to have a + // deterministic order of rollbacks. + List<RollbackInfo> sortedHighImpactRollbacks = highImpactRollbacks.stream().sorted( + Comparator.comparing(a -> a.getPackages().get(0).getPackageName())).toList(); + VersionedPackage firstRollback = + sortedHighImpactRollbacks + .get(0) + .getPackages() + .get(0) + .getVersionRolledBackFrom(); + Slog.i(TAG, "Rolling back high impact rollback for package: " + + firstRollback.getPackageName()); + rollbackPackage(sortedHighImpactRollbacks.get(0), firstRollback, rollbackReason); + } + + @WorkerThread + private void rollbackAll(@FailureReasons int rollbackReason) { + assertInWorkerThread(); + RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); + List<RollbackInfo> rollbacks = rollbackManager.getAvailableRollbacks(); + if (useTwoPhaseRollback(rollbacks)) { + return; + } + + Slog.i(TAG, "Rolling back all available rollbacks"); + // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all + // pending staged rollbacks are handled. + for (RollbackInfo rollback : rollbacks) { + if (rollback.isStaged()) { + mPendingStagedRollbackIds.add(rollback.getRollbackId()); + } + } + + for (RollbackInfo rollback : rollbacks) { + VersionedPackage firstRollback = + rollback.getPackages().get(0).getVersionRolledBackFrom(); + rollbackPackage(rollback, firstRollback, rollbackReason); + } + } + + /** + * Rollback all available low impact rollbacks + * @param availableRollbacks all available rollbacks + * @param rollbackReason reason to rollbacks + */ + @WorkerThread + private void rollbackAllLowImpact( + List<RollbackInfo> availableRollbacks, @FailureReasons int rollbackReason) { + assertInWorkerThread(); + + List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel( + availableRollbacks, + PackageManager.ROLLBACK_USER_IMPACT_LOW); + if (useTwoPhaseRollback(lowImpactRollbacks)) { + return; + } + + Slog.i(TAG, "Rolling back all available low impact rollbacks"); + logCrashRecoveryEvent(Log.DEBUG, "Rolling back all available. Reason: " + rollbackReason); + // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all + // pending staged rollbacks are handled. + for (RollbackInfo rollback : lowImpactRollbacks) { + if (rollback.isStaged()) { + mPendingStagedRollbackIds.add(rollback.getRollbackId()); + } + } + + for (RollbackInfo rollback : lowImpactRollbacks) { + VersionedPackage firstRollback = + rollback.getPackages().get(0).getVersionRolledBackFrom(); + rollbackPackage(rollback, firstRollback, rollbackReason); + } + } + + private List<RollbackInfo> getRollbacksAvailableForImpactLevel( + List<RollbackInfo> availableRollbacks, int impactLevel) { + return availableRollbacks.stream() + .filter(rollbackInfo -> rollbackInfo.getRollbackImpactLevel() == impactLevel) + .toList(); + } + + private int getMinRollbackImpactLevel(List<RollbackInfo> availableRollbacks) { + return availableRollbacks.stream() + .mapToInt(RollbackInfo::getRollbackImpactLevel) + .min() + .orElse(-1); + } + + private int getUserImpactBasedOnRollbackImpactLevel(List<RollbackInfo> availableRollbacks) { + int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + int minImpact = getMinRollbackImpactLevel(availableRollbacks); + switch (minImpact) { + case PackageManager.ROLLBACK_USER_IMPACT_LOW: + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; + break; + case PackageManager.ROLLBACK_USER_IMPACT_HIGH: + if (!SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) { + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_90; + } + break; + default: + impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + return impact; + } + + @VisibleForTesting + Handler getHandler() { + return mHandler; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java new file mode 100644 index 000000000000..9cfed02f9355 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.rollback; + +import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE; +import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.PackageWatchdog; +import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; + +import java.util.List; + +/** + * This class handles the logic for logging Watchdog-triggered rollback events. + * @hide + */ +public final class WatchdogRollbackLogger { + private static final String TAG = "WatchdogRollbackLogger"; + + private static final String LOGGING_PARENT_KEY = "android.content.pm.LOGGING_PARENT"; + + private WatchdogRollbackLogger() { + } + + @Nullable + private static String getLoggingParentName(Context context, @NonNull String packageName) { + PackageManager packageManager = context.getPackageManager(); + try { + int flags = PackageManager.MATCH_APEX | PackageManager.GET_META_DATA; + ApplicationInfo ai = packageManager.getPackageInfo(packageName, flags).applicationInfo; + if (ai.metaData == null) { + return null; + } + return ai.metaData.getString(LOGGING_PARENT_KEY); + } catch (Exception e) { + Slog.w(TAG, "Unable to discover logging parent package: " + packageName, e); + return null; + } + } + + /** + * Returns the logging parent of a given package if it exists, {@code null} otherwise. + * + * The logging parent is defined by the {@code android.content.pm.LOGGING_PARENT} field in the + * metadata of a package's AndroidManifest.xml. + */ + @VisibleForTesting + @Nullable + static VersionedPackage getLogPackage(Context context, + @NonNull VersionedPackage failingPackage) { + String logPackageName; + VersionedPackage loggingParent; + logPackageName = getLoggingParentName(context, failingPackage.getPackageName()); + if (logPackageName == null) { + return null; + } + try { + loggingParent = new VersionedPackage(logPackageName, context.getPackageManager() + .getPackageInfo(logPackageName, 0 /* flags */).getLongVersionCode()); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + return loggingParent; + } + + static void logRollbackStatusOnBoot(Context context, int rollbackId, String logPackageName, + List<RollbackInfo> recentlyCommittedRollbacks) { + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + + RollbackInfo rollback = null; + for (RollbackInfo info : recentlyCommittedRollbacks) { + if (rollbackId == info.getRollbackId()) { + rollback = info; + break; + } + } + + if (rollback == null) { + Slog.e(TAG, "rollback info not found for last staged rollback: " + rollbackId); + return; + } + + // Use the version of the logging parent that was installed before + // we rolled back for logging purposes. + VersionedPackage oldLoggingPackage = null; + if (!TextUtils.isEmpty(logPackageName)) { + for (PackageRollbackInfo packageRollback : rollback.getPackages()) { + if (logPackageName.equals(packageRollback.getPackageName())) { + oldLoggingPackage = packageRollback.getVersionRolledBackFrom(); + break; + } + } + } + + int sessionId = rollback.getCommittedSessionId(); + PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); + if (sessionInfo == null) { + Slog.e(TAG, "On boot completed, could not load session id " + sessionId); + return; + } + + if (sessionInfo.isStagedSessionApplied()) { + logEvent(oldLoggingPackage, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, ""); + } else if (sessionInfo.isStagedSessionFailed()) { + logEvent(oldLoggingPackage, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, + WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, ""); + } + } + + /** + * Log a Watchdog rollback event to statsd. + * + * @param logPackage the package to associate the rollback with. + * @param type the state of the rollback. + * @param rollbackReason the reason Watchdog triggered a rollback, if known. + * @param failingPackageName the failing package or process which triggered the rollback. + */ + public static void logEvent(@Nullable VersionedPackage logPackage, int type, + int rollbackReason, @NonNull String failingPackageName) { + String logMsg = "Watchdog event occurred with type: " + rollbackTypeToString(type) + + " logPackage: " + logPackage + + " rollbackReason: " + rollbackReasonToString(rollbackReason) + + " failedPackageName: " + failingPackageName; + Slog.i(TAG, logMsg); + if (logPackage != null) { + CrashRecoveryStatsLog.write( + CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED, + type, + logPackage.getPackageName(), + logPackage.getVersionCode(), + rollbackReason, + failingPackageName, + new byte[]{}); + } else { + // In the case that the log package is null, still log an empty string as an + // indication that retrieving the logging parent failed. + CrashRecoveryStatsLog.write( + CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED, + type, + "", + 0, + rollbackReason, + failingPackageName, + new byte[]{}); + } + + logTestProperties(logMsg); + } + + /** + * Writes properties which will be used by rollback tests to check if particular rollback + * events have occurred. + */ + private static void logTestProperties(String logMsg) { + // This property should be on only during the tests + if (!SystemProperties.getBoolean("persist.sys.rollbacktest.enabled", false)) { + return; + } + logCrashRecoveryEvent(Log.DEBUG, logMsg); + } + + @VisibleForTesting + static int mapFailureReasonToMetric(@PackageWatchdog.FailureReasons int failureReason) { + switch (failureReason) { + case PackageWatchdog.FAILURE_REASON_NATIVE_CRASH: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH; + case PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK; + case PackageWatchdog.FAILURE_REASON_APP_CRASH: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH; + case PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING; + case PackageWatchdog.FAILURE_REASON_BOOT_LOOP: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING; + default: + return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN; + } + } + + private static String rollbackTypeToString(int type) { + switch (type) { + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE: + return "ROLLBACK_INITIATE"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS: + return "ROLLBACK_SUCCESS"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE: + return "ROLLBACK_FAILURE"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED: + return "ROLLBACK_BOOT_TRIGGERED"; + default: + return "UNKNOWN"; + } + } + + private static String rollbackReasonToString(int reason) { + switch (reason) { + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH: + return "REASON_NATIVE_CRASH"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK: + return "REASON_EXPLICIT_HEALTH_CHECK"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH: + return "REASON_APP_CRASH"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING: + return "REASON_APP_NOT_RESPONDING"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT: + return "REASON_NATIVE_CRASH_DURING_BOOT"; + case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING: + return "REASON_BOOT_LOOP"; + default: + return "UNKNOWN"; + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java new file mode 100644 index 000000000000..29ff7cced897 --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.annotation.Nullable; + +/** + * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java + * + * @hide + */ +public class ArrayUtils { + private ArrayUtils() { /* cannot be instantiated */ } + + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable int[] array) { + return array == null || array.length == 0; + } + + /** + * True if the byte array is null or has length 0. + */ + public static boolean isEmpty(@Nullable byte[] array) { + return array == null || array.length == 0; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java new file mode 100644 index 000000000000..d60a9b9847ca --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Bits and pieces copied from hidden API of android.os.FileUtils. + * + * @hide + */ +public class FileUtils { + /** + * Read a text file into a String, optionally limiting the length. + * + * @param file to read (will not seek, so things like /proc files are OK) + * @param max length (positive for head, negative of tail, 0 for no limit) + * @param ellipsis to add of the file was truncated (can be null) + * @return the contents of the file, possibly truncated + * @throws IOException if something goes wrong reading the file + * @hide + */ + public static @Nullable String readTextFile(@Nullable File file, @Nullable int max, + @Nullable String ellipsis) throws IOException { + InputStream input = new FileInputStream(file); + // wrapping a BufferedInputStream around it because when reading /proc with unbuffered + // input stream, bytes read not equal to buffer size is not necessarily the correct + // indication for EOF; but it is true for BufferedInputStream due to its implementation. + BufferedInputStream bis = new BufferedInputStream(input); + try { + long size = file.length(); + if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes + if (size > 0 && (max == 0 || size < max)) max = (int) size; + byte[] data = new byte[max + 1]; + int length = bis.read(data); + if (length <= 0) return ""; + if (length <= max) return new String(data, 0, length); + if (ellipsis == null) return new String(data, 0, max); + return new String(data, 0, max) + ellipsis; + } else if (max < 0) { // "tail" mode: keep the last N + int len; + boolean rolled = false; + byte[] last = null; + byte[] data = null; + do { + if (last != null) rolled = true; + byte[] tmp = last; + last = data; + data = tmp; + if (data == null) data = new byte[-max]; + len = bis.read(data); + } while (len == data.length); + + if (last == null && len <= 0) return ""; + if (last == null) return new String(data, 0, len); + if (len > 0) { + rolled = true; + System.arraycopy(last, len, last, 0, last.length - len); + System.arraycopy(data, 0, last, last.length - len, len); + } + if (ellipsis == null || !rolled) return new String(last); + return ellipsis + new String(last); + } else { // "cat" mode: size unknown, read it all in streaming fashion + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + int len; + byte[] data = new byte[1024]; + do { + len = bis.read(data); + if (len > 0) contents.write(data, 0, len); + } while (len == data.length); + return contents.toString(); + } + } finally { + bis.close(); + input.close(); + } + } + + /** + * Perform an fsync on the given FileOutputStream. The stream at this + * point must be flushed but not yet closed. + * + * @hide + */ + public static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java new file mode 100644 index 000000000000..9a24ada8b69a --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import libcore.util.EmptyArray; + +import java.util.NoSuchElementException; + +/** + * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java + * + * @hide + */ +public class LongArrayQueue { + + private long[] mValues; + private int mSize; + private int mHead; + private int mTail; + + private long[] newUnpaddedLongArray(int num) { + return new long[num]; + } + /** + * Initializes a queue with the given starting capacity. + * + * @param initialCapacity the capacity. + */ + public LongArrayQueue(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + mHead = mTail = 0; + } + + /** + * Initializes a queue with default starting capacity. + */ + public LongArrayQueue() { + this(16); + } + + /** @hide */ + public static int growSize(int currentSize) { + return currentSize <= 4 ? 8 : currentSize * 2; + } + + private void grow() { + if (mSize < mValues.length) { + throw new IllegalStateException("Queue not full yet!"); + } + final int newSize = growSize(mSize); + final long[] newArray = newUnpaddedLongArray(newSize); + final int r = mValues.length - mHead; // Number of elements on and to the right of head. + System.arraycopy(mValues, mHead, newArray, 0, r); + System.arraycopy(mValues, 0, newArray, r, mHead); + mValues = newArray; + mHead = 0; + mTail = mSize; + } + + /** + * Returns the number of elements in the queue. + */ + public int size() { + return mSize; + } + + /** + * Removes all elements from this queue. + */ + public void clear() { + mSize = 0; + mHead = mTail = 0; + } + + /** + * Adds a value to the tail of the queue. + * + * @param value the value to be added. + */ + public void addLast(long value) { + if (mSize == mValues.length) { + grow(); + } + mValues[mTail] = value; + mTail = (mTail + 1) % mValues.length; + mSize++; + } + + /** + * Removes an element from the head of the queue. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long removeFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final long ret = mValues[mHead]; + mHead = (mHead + 1) % mValues.length; + mSize--; + return ret; + } + + /** + * Returns the element at the given position from the head of the queue, where 0 represents the + * head of the queue. + * + * @param position the position from the head of the queue. + * @return the element found at the given position. + * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or + * {@code position} >= {@link #size()} + */ + public long get(int position) { + if (position < 0 || position >= mSize) { + throw new IndexOutOfBoundsException("Index " + position + + " not valid for a queue of size " + mSize); + } + final int index = (mHead + position) % mValues.length; + return mValues[index]; + } + + /** + * Returns the element at the head of the queue, without removing it. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty + */ + public long peekFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + return mValues[mHead]; + } + + /** + * Returns the element at the tail of the queue. + * + * @return the element at the tail of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long peekLast() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; + return mValues[index]; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + if (mSize <= 0) { + return "{}"; + } + + final StringBuilder buffer = new StringBuilder(mSize * 64); + buffer.append('{'); + buffer.append(get(0)); + for (int i = 1; i < mSize; i++) { + buffer.append(", "); + buffer.append(get(i)); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java new file mode 100644 index 000000000000..488b531c2b8a --- /dev/null +++ b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * Bits and pieces copied from hidden API of + * frameworks/base/core/java/com/android/internal/util/XmlUtils.java + * + * @hide + */ +public class XmlUtils { + + /** @hide */ + public static final void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != parser.START_TAG + && type != parser.END_DOCUMENT) { + // Do nothing + } + + if (type != parser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + /** @hide */ + public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) + throws IOException, XmlPullParserException { + for (;;) { + int type = parser.next(); + if (type == XmlPullParser.END_DOCUMENT + || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { + return false; + } + if (type == XmlPullParser.START_TAG + && parser.getDepth() == outerDepth + 1) { + return true; + } + } + } +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 39a7aea67078..2d68ab8ff451 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -493,6 +493,14 @@ flag { } flag { + name: "status_bar_no_hun_behavior" + namespace: "systemui" + description: "When there's a HUN, don't show the HUN text or icon in the status bar. Instead, " + "continue showing the usual status bar." + bug: "385740230" +} + +flag { name: "promote_notifications_automatically" namespace: "systemui" description: "Flag to automatically turn certain notifications into promoted notifications so " @@ -617,13 +625,6 @@ flag { } flag { - name: "status_bar_connected_displays" - namespace: "lse_desktop_experience" - description: "Shows the status bar on connected displays" - bug: "379264862" -} - -flag { name: "status_bar_switch_to_spn_from_data_spn" namespace: "systemui" description: "Fix usage of the SPN broadcast extras" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt index d86c6efce284..ba73504b2a03 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.icon.ui.viewmodel import android.graphics.Rect import android.graphics.drawable.Icon +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -36,6 +38,7 @@ import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -258,6 +261,7 @@ class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterizati } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun isolatedIcon_animateOnAppear_shadeCollapsed() = testScope.runTest { val icon: Icon = mock() @@ -285,6 +289,7 @@ class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterizati } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun isolatedIcon_dontAnimateOnAppear_shadeExpanded() = testScope.runTest { val icon: Icon = mock() @@ -312,6 +317,7 @@ class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterizati } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun isolatedIcon_updateWhenIconDataChanges() = testScope.runTest { val icon: Icon = mock() @@ -339,6 +345,7 @@ class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterizati } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun isolatedIcon_lastMessageIsFromReply_notNull() = testScope.runTest { val icon: Icon = mock() @@ -362,4 +369,32 @@ class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterizati assertThat(isolatedIcon?.value?.notifKey).isEqualTo("notif1") } + + @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isolatedIcon_noHunBehaviorFlagEnabled_doesNothing() = + testScope.runTest { + val icon: Icon = mock() + val isolatedIcon by collectLastValue(underTest.isolatedIcon) + runCurrent() + + headsUpViewStateRepository.isolatedNotification.value = "notif1" + runCurrent() + + activeNotificationsRepository.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { + addIndividualNotif( + activeNotificationModel( + key = "notif1", + groupKey = "group", + statusBarIcon = icon, + ) + ) + } + .build() + runCurrent() + + assertThat(isolatedIcon?.value).isNull() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt index 216f51d992d0..bd76268d2cfa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.shade.shadeViewController import com.android.systemui.statusbar.HeadsUpStatusBarView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.commandQueue +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor @@ -118,6 +119,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun showingEntryUpdated_whenPinnedBySystem() { row.setPinnedStatus(PinnedStatus.PinnedBySystem) setHeadsUpNotifOnManager(entry) @@ -133,8 +135,18 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun showingEntryUpdated_whenPinnedByUser_andFlagOff() { + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun showingEntryNotUpdated_whenPinnedBySystem_whenNoHunBehaviorEnabled() { + row.setPinnedStatus(PinnedStatus.PinnedBySystem) + setHeadsUpNotifOnManager(entry) + underTest.onHeadsUpPinned(entry) + + assertThat(headsUpStatusBarView.showingEntry).isNull() + } + + @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarNoHunBehavior.FLAG_NAME) + fun showingEntryUpdated_whenPinnedByUser_andNotifChipsFlagOff() { row.setPinnedStatus(PinnedStatus.PinnedByUser) setHeadsUpNotifOnManager(entry) underTest.onHeadsUpPinned(entry) @@ -144,7 +156,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showingEntryNotUpdated_whenPinnedByUser_andFlagOn() { + fun showingEntryNotUpdated_whenPinnedByUser_andNotifChipsFlagOn() { // WHEN the HUN was pinned by the user row.setPinnedStatus(PinnedStatus.PinnedByUser) setHeadsUpNotifOnManager(entry) @@ -155,6 +167,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun pinnedStatusUpdatedToSystem_whenPinnedBySystem() { row.setPinnedStatus(PinnedStatus.PinnedBySystem) setHeadsUpNotifOnManager(entry) @@ -168,8 +181,19 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun pinnedStatusNotUpdatedToSystem_whenPinnedBySystem_whenNoHunBehaviorEnabled() { + row.setPinnedStatus(PinnedStatus.PinnedBySystem) + setHeadsUpNotifOnManager(entry) + underTest.onHeadsUpPinned(entry) + + assertThat(underTest.pinnedStatus).isEqualTo(PinnedStatus.NotPinned) + } + + @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun pinnedStatusUpdatedToNotPinned_whenPinnedByUser_andFlagOn() { + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun pinnedStatusUpdatedToNotPinned_whenPinnedByUser_andNotifChipsFlagOn() { row.setPinnedStatus(PinnedStatus.PinnedByUser) setHeadsUpNotifOnManager(entry) underTest.onHeadsUpPinned(entry) @@ -182,6 +206,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun isolatedIconSet_whenPinnedBySystem() = kosmos.runTest { val latestIsolatedIcon by @@ -201,8 +226,22 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun isolatedIconSet_whenPinnedByUser_andFlagOff() = + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isolatedIconNotSet_whenPinnedBySystem_whenNoHunBehaviorEnabled() = + kosmos.runTest { + val latestIsolatedIcon by + collectLastValue(kosmos.headsUpNotificationIconInteractor.isolatedNotification) + + row.setPinnedStatus(PinnedStatus.PinnedBySystem) + setHeadsUpNotifOnManager(entry) + underTest.onHeadsUpPinned(entry) + + assertThat(latestIsolatedIcon).isNull() + } + + @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarNoHunBehavior.FLAG_NAME) + fun isolatedIconSet_whenPinnedByUser_andNotifChipsFlagOff() = kosmos.runTest { val latestIsolatedIcon by collectLastValue(kosmos.headsUpNotificationIconInteractor.isolatedNotification) @@ -216,7 +255,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun isolatedIconNotSet_whenPinnedByUser_andFlagOn() = + fun isolatedIconNotSet_whenPinnedByUser_andNotifChipsFlagOn() = kosmos.runTest { val latestIsolatedIcon by collectLastValue(kosmos.headsUpNotificationIconInteractor.isolatedNotification) @@ -243,6 +282,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun operatorNameViewUpdated_whenPinnedBySystem() { underTest.setAnimationsEnabled(false) @@ -258,8 +298,20 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { } @Test - @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun operatorNameViewUpdated_whenPinnedByUser_andFlagOff() { + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun operatorNameViewNotUpdated_whenPinnedBySystem_whenNoHunBehaviorEnabled() { + underTest.setAnimationsEnabled(false) + + row.setPinnedStatus(PinnedStatus.PinnedBySystem) + setHeadsUpNotifOnManager(entry) + underTest.onHeadsUpPinned(entry) + + assertThat(operatorNameView.visibility).isEqualTo(View.VISIBLE) + } + + @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarNoHunBehavior.FLAG_NAME) + fun operatorNameViewUpdated_whenPinnedByUser_andNotifChipsFlagOff() { underTest.setAnimationsEnabled(false) row.setPinnedStatus(PinnedStatus.PinnedByUser) @@ -271,7 +323,7 @@ class HeadsUpAppearanceControllerTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun operatorNameViewNotUpdated_whenPinnedByUser_andFlagOn() { + fun operatorNameViewNotUpdated_whenPinnedByUser_andNotifChipsFlagOn() { underTest.setAnimationsEnabled(false) // WHEN the row was pinned by the user diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index be4af868b740..03abcf850d26 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -70,6 +70,7 @@ import com.android.systemui.statusbar.events.data.repository.systemStatusEventAn import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository @@ -552,9 +553,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun shouldShowOperatorNameView_allowedByInteractor_hunPinned_false() = + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun shouldShowOperatorNameView_allowedByInteractor_hunPinned_noHunBehaviorFlagOff_false() = kosmos.runTest { - kosmos.setHomeStatusBarInteractorShowOperatorName(false) + kosmos.setHomeStatusBarInteractorShowOperatorName(true) transitionKeyguardToGone() @@ -565,7 +567,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { headsUpNotificationRepository.setNotifications( UnconfinedFakeHeadsUpRowRepository( key = "key", - pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), ) ) @@ -575,6 +577,31 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun shouldShowOperatorNameView_allowedByInteractor_hunPinned_noHunBehaviorFlagOn_true() = + kosmos.runTest { + kosmos.setHomeStatusBarInteractorShowOperatorName(true) + + transitionKeyguardToGone() + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + + // WHEN there is an active HUN + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + val latest by collectLastValue(underTest.shouldShowOperatorNameView) + + // THEN we still show the operator name view if NoHunBehavior flag is enabled + assertThat(latest).isTrue() + } + + @Test fun shouldHomeStatusBarBeVisible_keyguardNotGone_noHun_false() = kosmos.runTest { // Do not transition from keyguard. i.e., we don't call transitionKeyguardToGone() @@ -692,7 +719,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun isClockVisible_allowedByFlags_hunPinnedByUser_visible() = + fun isClockVisible_allowedByDisableFlags_hunPinnedByUser_visible() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) transitionKeyguardToGone() @@ -711,7 +738,8 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isClockVisible_allowedByFlags_hunPinnedBySystem_notVisible() = + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isClockVisible_allowedByDisableFlags_hunPinnedBySystem_noHunBehaviorFlagOff_notVisible() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) transitionKeyguardToGone() @@ -730,7 +758,29 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isClockVisible_allowedByFlags_hunBecomesInactive_visibleAgain() = + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isClockVisible_allowedByDisableFlags_hunPinnedBySystem_noHunBehaviorFlagOn_visible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isClockVisible) + transitionKeyguardToGone() + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + // WHEN there is an active HUN + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + // THEN we still show the clock view if NoHunBehavior flag is enabled + assertThat(latest!!.visibility).isEqualTo(View.VISIBLE) + } + + @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isClockVisible_allowedByDisableFlags_hunBecomesInactive_visibleAgain() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) transitionKeyguardToGone() @@ -753,7 +803,8 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isClockVisible_disabledByFlags_hunBecomesInactive_neverVisible() = + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isClockVisible_disableFlagsProhibitClock_hunBecomesInactive_neverVisible() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) transitionKeyguardToGone() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index 0598b87aec9d..73e5004d47f0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.ui.viewmodel +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -32,6 +34,7 @@ import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -127,7 +130,8 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa @Test @EnableSceneContainer - fun isVisible_headsUpStatusBarShown_false() = + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isVisible_headsUpShown_noHunBehaviorFlagOff_false() = testScope.runTest { val latest by collectLastValue(underTest.isVisible) @@ -145,6 +149,26 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test + @EnableSceneContainer + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isVisible_headsUpShown_noHunBehaviorFlagOn_true() = + testScope.runTest { + val latest by collectLastValue(underTest.isVisible) + + // WHEN HUN displayed on the bypass lock screen + headsUpRepository.setNotifications(FakeHeadsUpRowRepository("key 0", isPinned = true)) + keyguardTransitionRepository.emitInitialStepsFromOff( + KeyguardState.LOCKSCREEN, + testSetup = true, + ) + kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) + faceAuthRepository.isBypassEnabled.value = true + + // THEN KeyguardStatusBar is still visible because StatusBarNoHunBehavior is enabled + assertThat(latest).isTrue() + } + + @Test fun isVisible_sceneLockscreen_andNotDozing_andNotShowingHeadsUpStatusBar_true() = testScope.runTest { val latest by collectLastValue(underTest.isVisible) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index 51892aac606a..ff6bcdb150f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -19,6 +19,7 @@ package com.android.systemui.shared.system; import android.graphics.Rect; import android.os.Bundle; import android.view.RemoteAnimationTarget; +import android.window.TransitionInfo; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -30,7 +31,7 @@ public interface RecentsAnimationListener { */ void onAnimationStart(RecentsAnimationControllerCompat controller, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, - Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras); + Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras, TransitionInfo info); /** * Called when the animation into Recents was canceled. This call is made on the binder thread. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt new file mode 100644 index 000000000000..2ae54d7c6c83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.headsup.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the status bar no hun behavior flag state. */ +@Suppress("NOTHING_TO_INLINE") +object StatusBarNoHunBehavior { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_NO_HUN_BEHAVIOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.statusBarNoHunBehavior() && android.app.Flags.notificationsRedesignAppIcons() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is enabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationIconInteractor.kt index 17b6e9f572c9..e5cc3b973c51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationIconInteractor.kt @@ -16,16 +16,19 @@ package com.android.systemui.statusbar.notification.domain.interactor import android.graphics.Rect +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationIconViewStateRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow -/** Domain logic pertaining to heads up notification icons. */ +/** + * Domain logic pertaining to heads up notification icons. + * + * If [StatusBarNoHunBehavior] is enabled, this class should do nothing. + */ class HeadsUpNotificationIconInteractor @Inject -constructor( - private val repository: HeadsUpNotificationIconViewStateRepository, -) { +constructor(private val repository: HeadsUpNotificationIconViewStateRepository) { /** Notification key for a notification icon to show isolated, or `null` if none. */ val isolatedIconLocation: Flow<Rect?> = repository.isolatedIconLocation @@ -34,11 +37,13 @@ constructor( /** Updates the location where isolated notification icons are shown. */ fun setIsolatedIconLocation(rect: Rect?) { + StatusBarNoHunBehavior.assertInLegacyMode() repository.isolatedIconLocation.value = rect } /** Updates which notification will have its icon displayed isolated. */ fun setIsolatedIconNotificationKey(key: String?) { + StatusBarNoHunBehavior.assertInLegacyMode() repository.isolatedNotification.value = key } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt index 643ee249e75e..348552f81fa7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt @@ -32,6 +32,7 @@ import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore @@ -81,7 +82,9 @@ object NotificationIconContainerViewBinder { StatusBarIconViewBinder.bindIconColors(sbiv, iconColors, contrastColorUtil) } } - launch { viewModel.bindIsolatedIcon(view, viewStore) } + if (!StatusBarNoHunBehavior.isEnabled) { + launch { viewModel.bindIsolatedIcon(view, viewStore) } + } launch { viewModel.animationsEnabled.bindAnimationsEnabled(view) } } @@ -146,6 +149,7 @@ object NotificationIconContainerViewBinder { view: NotificationIconContainer, viewStore: IconViewStore, ) { + StatusBarNoHunBehavior.assertInLegacyMode() coroutineScope { launch { isolatedIconLocation.collectTracingEach("NIC#isolatedIconLocation") { location -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt index e1032820fb71..8f43c323aeb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor import com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor @@ -37,7 +38,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -92,31 +95,42 @@ constructor( /** An Icon to show "isolated" in the IconContainer. */ val isolatedIcon: Flow<AnimatedValue<NotificationIconInfo?>> = - headsUpIconInteractor.isolatedNotification - .combine(icons) { isolatedNotif, iconsViewData -> - isolatedNotif?.let { - iconsViewData.visibleIcons.firstOrNull { it.notifKey == isolatedNotif } - } - } - .distinctUntilChanged() - .flowOn(bgContext) - .conflate() - .distinctUntilChanged() - .pairwise(initialValue = null) - .sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion -> - val animate = - when { - iconInfo?.notifKey == prev?.notifKey -> false - iconInfo == null || prev == null -> shadeExpansion == 0f - else -> false + if (StatusBarNoHunBehavior.isEnabled) { + flowOf(AnimatedValue.NotAnimating(null)) + } else { + headsUpIconInteractor.isolatedNotification + .combine(icons) { isolatedNotif, iconsViewData -> + isolatedNotif?.let { + iconsViewData.visibleIcons.firstOrNull { it.notifKey == isolatedNotif } } - AnimatableEvent(iconInfo, animate) - } - .toAnimatedValueFlow() + } + .distinctUntilChanged() + .flowOn(bgContext) + .conflate() + .distinctUntilChanged() + .pairwise(initialValue = null) + .sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion -> + val animate = + when { + iconInfo?.notifKey == prev?.notifKey -> false + iconInfo == null || prev == null -> shadeExpansion == 0f + else -> false + } + AnimatableEvent(iconInfo, animate) + } + .toAnimatedValueFlow() + } /** Location to show an isolated icon, if there is one. */ val isolatedIconLocation: Flow<Rect> = - headsUpIconInteractor.isolatedIconLocation.filterNotNull().conflate().distinctUntilChanged() + if (StatusBarNoHunBehavior.isEnabled) { + emptyFlow() + } else { + headsUpIconInteractor.isolatedIconLocation + .filterNotNull() + .conflate() + .distinctUntilChanged() + } private class IconColorsImpl(override val tint: Int, private val areas: Collection<Rect>) : NotificationIconColors { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index a8a1318664f2..495b50869458 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -98,6 +98,7 @@ import com.android.systemui.shade.QSHeaderBoundsProvider; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; @@ -5339,7 +5340,7 @@ public class NotificationStackScrollLayout void onStatePostChange(boolean fromShadeLocked) { boolean onKeyguard = onKeyguard(); - if (mHeadsUpAppearanceController != null) { + if (mHeadsUpAppearanceController != null && !StatusBarNoHunBehavior.isEnabled()) { mHeadsUpAppearanceController.onStateChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index 53a29505510b..548ab8311144 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -39,6 +39,7 @@ import com.android.systemui.statusbar.HeadsUpStatusBarView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.core.StatusBarRootModernization; +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -151,19 +152,21 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar mOperatorNameViewOptional = operatorNameViewOptional; mDarkIconDispatcher = darkIconDispatcher; - mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - if (shouldHeadsUpStatusBarBeVisible()) { - updateTopEntry(); - - // trigger scroller to notify the latest panel translation - mStackScrollerController.requestLayout(); + if (!StatusBarNoHunBehavior.isEnabled()) { + mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (shouldHeadsUpStatusBarBeVisible()) { + updatePinnedStatus(); + + // trigger scroller to notify the latest panel translation + mStackScrollerController.requestLayout(); + } + mView.removeOnLayoutChangeListener(this); } - mView.removeOnLayoutChangeListener(this); - } - }); + }); + } mBypassController = bypassController; mStatusBarStateController = stateController; mPhoneStatusBarTransitions = phoneStatusBarTransitions; @@ -175,13 +178,15 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @Override protected void onViewAttached() { mHeadsUpManager.addListener(this); - mView.setOnDrawingRectChangedListener(this::updateIsolatedIconLocation); - updateIsolatedIconLocation(); - mWakeUpCoordinator.addListener(this); + if (!StatusBarNoHunBehavior.isEnabled()) { + mView.setOnDrawingRectChangedListener(this::updateIsolatedIconLocation); + updateIsolatedIconLocation(); + mDarkIconDispatcher.addDarkReceiver(this); + mWakeUpCoordinator.addListener(this); + } getShadeHeadsUpTracker().addTrackingHeadsUpListener(mSetTrackingHeadsUp); getShadeHeadsUpTracker().setHeadsUpAppearanceController(this); mStackScrollerController.addOnExpandedHeightChangedListener(mSetExpandedHeight); - mDarkIconDispatcher.addDarkReceiver(this); } private ShadeHeadsUpTracker getShadeHeadsUpTracker() { @@ -191,22 +196,25 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @Override protected void onViewDetached() { mHeadsUpManager.removeListener(this); - mView.setOnDrawingRectChangedListener(null); - mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(null); - mWakeUpCoordinator.removeListener(this); + if (!StatusBarNoHunBehavior.isEnabled()) { + mView.setOnDrawingRectChangedListener(null); + mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(null); + mDarkIconDispatcher.removeDarkReceiver(this); + mWakeUpCoordinator.removeListener(this); + } getShadeHeadsUpTracker().removeTrackingHeadsUpListener(mSetTrackingHeadsUp); getShadeHeadsUpTracker().setHeadsUpAppearanceController(null); mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight); - mDarkIconDispatcher.removeDarkReceiver(this); } private void updateIsolatedIconLocation() { + StatusBarNoHunBehavior.assertInLegacyMode(); mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(mView.getIconDrawingRect()); } @Override public void onHeadsUpPinned(NotificationEntry entry) { - updateTopEntry(); + updatePinnedStatus(); updateHeader(entry); updateHeadsUpAndPulsingRoundness(entry); } @@ -217,7 +225,10 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar mPhoneStatusBarTransitions.onHeadsUpStateChanged(isHeadsUp); } - private void updateTopEntry() { + private void updatePinnedStatus() { + if (StatusBarNoHunBehavior.isEnabled()) { + return; + } NotificationEntry newEntry = null; if (shouldHeadsUpStatusBarBeVisible()) { newEntry = mHeadsUpManager.getTopEntry(); @@ -239,6 +250,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar } private static @Nullable String getIsolatedIconKey(NotificationEntry newEntry) { + StatusBarNoHunBehavior.assertInLegacyMode(); if (newEntry == null) { return null; } @@ -259,6 +271,9 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar } private void setPinnedStatus(PinnedStatus pinnedStatus) { + if (StatusBarNoHunBehavior.isEnabled()) { + return; + } if (mPinnedStatus != pinnedStatus) { mPinnedStatus = pinnedStatus; @@ -292,6 +307,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar } private void updateParentClipping(boolean shouldClip) { + StatusBarNoHunBehavior.assertInLegacyMode(); ViewClippingUtil.setClippingDeactivated( mView, !shouldClip, mParentClippingParams); } @@ -319,6 +335,8 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar * */ private void hide(View view, int endState, Runnable callback) { + StatusBarNoHunBehavior.assertInLegacyMode(); + if (mAnimationsEnabled) { CrossFadeHelper.fadeOut(view, CONTENT_FADE_DURATION /* duration */, 0 /* delay */, () -> { @@ -336,6 +354,8 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar } private void show(View view) { + StatusBarNoHunBehavior.assertInLegacyMode(); + if (mAnimationsEnabled) { CrossFadeHelper.fadeIn(view, CONTENT_FADE_DURATION /* duration */, CONTENT_FADE_DELAY /* delay */); @@ -351,6 +371,9 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @VisibleForTesting public PinnedStatus getPinnedStatus() { + if (StatusBarNoHunBehavior.isEnabled()) { + return PinnedStatus.NotPinned; + } return mPinnedStatus; } @@ -375,6 +398,10 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar */ @Deprecated public boolean shouldHeadsUpStatusBarBeVisible() { + if (StatusBarNoHunBehavior.isEnabled()) { + return false; + } + if (StatusBarNotifChips.isEnabled()) { return canShowHeadsUp() && mHeadsUpManager.pinnedHeadsUpStatus() == PinnedStatus.PinnedBySystem; @@ -388,7 +415,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @Override public void onHeadsUpUnPinned(NotificationEntry entry) { - updateTopEntry(); + updatePinnedStatus(); updateHeader(entry); updateHeadsUpAndPulsingRoundness(entry); } @@ -406,7 +433,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar updateHeadsUpHeaders(); } if (isExpanded() != oldIsExpanded) { - updateTopEntry(); + updatePinnedStatus(); } } @@ -476,15 +503,18 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @Override public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) { + StatusBarNoHunBehavior.assertInLegacyMode(); mView.onDarkChanged(areas, darkIntensity, tint); } public void onStateChanged() { - updateTopEntry(); + StatusBarNoHunBehavior.assertInLegacyMode(); + updatePinnedStatus(); } @Override public void onFullyHiddenChanged(boolean isFullyHidden) { - updateTopEntry(); + StatusBarNoHunBehavior.assertInLegacyMode(); + updatePinnedStatus(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index c396512ce3a5..1a97ab635028 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -41,6 +41,7 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.settingslib.Utils; import com.android.systemui.res.R; import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.stack.AnimationFilter; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -163,12 +164,12 @@ public class NotificationIconContainer extends ViewGroup { private IconState mFirstVisibleIconState; private float mVisualOverflowStart; private boolean mIsShowingOverflowDot; - private StatusBarIconView mIsolatedIcon; - private Rect mIsolatedIconLocation; + @Nullable private StatusBarIconView mIsolatedIcon; + @Nullable private Rect mIsolatedIconLocation; private final int[] mAbsolutePosition = new int[2]; - private View mIsolatedIconForAnimation; + @Nullable private View mIsolatedIconForAnimation; private int mThemedTextColorPrimary; - private Runnable mIsolatedIconAnimationEndRunnable; + @Nullable private Runnable mIsolatedIconAnimationEndRunnable; private boolean mUseIncreasedIconScale; public NotificationIconContainer(Context context, AttributeSet attrs) { @@ -379,6 +380,9 @@ public class NotificationIconContainer extends ViewGroup { if (areAnimationsEnabled(icon) && !isReplacingIcon) { addTransientView(icon, 0); boolean isIsolatedIcon = child == mIsolatedIcon; + if (StatusBarNoHunBehavior.isEnabled()) { + isIsolatedIcon = false; + } icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */, () -> removeTransientView(icon), isIsolatedIcon ? CONTENT_FADE_DURATION : 0); @@ -539,7 +543,7 @@ public class NotificationIconContainer extends ViewGroup { iconState.setXTranslation(getRtlIconTranslationX(iconState, view)); } } - if (mIsolatedIcon != null) { + if (!StatusBarNoHunBehavior.isEnabled() && mIsolatedIcon != null) { IconState iconState = mIconStates.get(mIsolatedIcon); if (iconState != null) { // Most of the time the icon isn't yet added when this is called but only happening @@ -685,17 +689,20 @@ public class NotificationIconContainer extends ViewGroup { public void showIconIsolatedAnimated(StatusBarIconView icon, @Nullable Runnable onAnimationEnd) { + StatusBarNoHunBehavior.assertInLegacyMode(); mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon; mIsolatedIconAnimationEndRunnable = onAnimationEnd; showIconIsolated(icon); } public void showIconIsolated(StatusBarIconView icon) { + StatusBarNoHunBehavior.assertInLegacyMode(); mIsolatedIcon = icon; updateState(); } public void setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate) { + StatusBarNoHunBehavior.assertInLegacyMode(); mIsolatedIconLocation = isolatedIconLocation; if (requireUpdate) { updateState(); @@ -794,7 +801,7 @@ public class NotificationIconContainer extends ViewGroup { animationProperties.setDuration(CANNED_ANIMATION_DURATION); animate = true; } - if (mIsolatedIconForAnimation != null) { + if (!StatusBarNoHunBehavior.isEnabled() && mIsolatedIconForAnimation != null) { if (view == mIsolatedIconForAnimation) { animationProperties = UNISOLATION_PROPERTY; animationProperties.setDelay( @@ -843,6 +850,7 @@ public class NotificationIconContainer extends ViewGroup { @Nullable private Consumer<Property> getEndAction() { + if (StatusBarNoHunBehavior.isEnabled()) return null; if (mIsolatedIconAnimationEndRunnable == null) return null; final Runnable endRunnable = mIsolatedIconAnimationEndRunnable; return prop -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 1f1be261a854..c541cff4448c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -65,6 +65,7 @@ import com.android.systemui.statusbar.data.repository.StatusBarConfigurationCont import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.phone.PhoneStatusBarView; @@ -676,6 +677,11 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue boolean headsUpVisible = mHomeStatusBarComponent .getHeadsUpAppearanceController() .shouldHeadsUpStatusBarBeVisible(); + if (StatusBarNoHunBehavior.isEnabled()) { + // With this flag enabled, we have no custom HUN behavior, so just always consider it + // to be not visible. + headsUpVisible = false; + } if (SceneContainerFlag.isEnabled()) { // With the scene container, only use the value calculated by the view model to diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index 6ff4354fcc46..dcd2dbf57b42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor @@ -297,17 +298,32 @@ constructor( primaryOngoingActivityChip.map { it is OngoingActivityChipModel.Shown } } + /** + * True if we need to hide the usual start side content in order to show the heads up + * notification info. + */ + private val hideStartSideContentForHeadsUp: Flow<Boolean> = + if (StatusBarNoHunBehavior.isEnabled) { + flowOf(false) + } else { + headsUpNotificationInteractor.statusBarHeadsUpStatus.map { + it == PinnedStatus.PinnedBySystem + } + } + override val shouldShowOperatorNameView: Flow<Boolean> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpStatus, + hideStartSideContentForHeadsUp, homeStatusBarInteractor.visibilityViaDisableFlags, homeStatusBarInteractor.shouldShowOperatorName, - ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator - -> - val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem + ) { + shouldStatusBarBeVisible, + hideStartSideContentForHeadsUp, + visibilityViaDisableFlags, + shouldShowOperator -> shouldStatusBarBeVisible && - !hideForHeadsUp && + !hideStartSideContentForHeadsUp && visibilityViaDisableFlags.isSystemInfoAllowed && shouldShowOperator } @@ -315,14 +331,13 @@ constructor( override val isClockVisible: Flow<VisibilityModel> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpStatus, + hideStartSideContentForHeadsUp, homeStatusBarInteractor.visibilityViaDisableFlags, - ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags -> - val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, hideStartSideContentForHeadsUp, visibilityViaDisableFlags -> val showClock = shouldStatusBarBeVisible && visibilityViaDisableFlags.isClockAllowed && - !hideClockForHeadsUp + !hideStartSideContentForHeadsUp // Always use View.INVISIBLE here, so that animations work VisibilityModel(showClock.toVisibleOrInvisible(), visibilityViaDisableFlags.animate) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index a98a9e0c16d2..12ef68dafa64 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -59,7 +60,7 @@ constructor( ) { private val showingHeadsUpStatusBar: Flow<Boolean> = - if (SceneContainerFlag.isEnabled) { + if (SceneContainerFlag.isEnabled && !StatusBarNoHunBehavior.isEnabled) { headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned } } else { flowOf(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index f9df6c79e140..0d0415e0d72d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -67,6 +67,7 @@ import com.android.systemui.statusbar.data.repository.StatusBarConfigurationCont import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerStore; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager; @@ -624,13 +625,14 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility()); } - @Test - @DisableFlags({ - StatusBarNotifChips.FLAG_NAME, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void hasOngoingActivityButAlsoHun_chipHidden_notifsFlagOff() { + @Test + @DisableFlags({ + StatusBarNotifChips.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarNoHunBehavior.FLAG_NAME, + }) + public void hasOngoingActivityButAlsoHun_chipHidden_notifChipsFlagOff() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( @@ -646,8 +648,12 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({StatusBarNotifChips.FLAG_NAME}) - @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) - public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() { + @DisableFlags({ + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarNoHunBehavior.FLAG_NAME + }) + public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifChipsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( @@ -662,13 +668,31 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility()); } + @Test + @EnableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarNoHunBehavior.FLAG_NAME}) + @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) + public void hasOngoingActivitiesButAlsoHun_noHunBehaviorFlagOn_chipsNotHidden() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasPrimaryOngoingActivity= */ true, + /* hasSecondaryOngoingActivity= */ true, + /* shouldAnimate= */ false); + when(mHeadsUpAppearanceController.shouldHeadsUpStatusBarBeVisible()).thenReturn(true); + + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility()); + assertEquals(View.VISIBLE, getSecondaryOngoingActivityChipView().getVisibility()); + } + @Test @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME }) - public void primaryOngoingActivityEnded_chipHidden_notifsFlagOff() { + public void primaryOngoingActivityEnded_chipHidden_notifChipsFlagOff() { resumeAndGetFragment(); // Ongoing activity started @@ -948,9 +972,13 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { assertEquals(View.VISIBLE, getClockView().getVisibility()); } - @Test - @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) - public void disable_shouldHeadsUpStatusBarBeVisibleTrue_clockDisabled() { + @Test + @DisableFlags({ + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarNoHunBehavior.FLAG_NAME, + }) + public void disable_shouldHeadsUpStatusBarBeVisibleTrue_clockDisabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mHeadsUpAppearanceController.shouldHeadsUpStatusBarBeVisible()).thenReturn(true); @@ -960,7 +988,11 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) + @DisableFlags({ + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarNoHunBehavior.FLAG_NAME, + }) public void disable_shouldHeadsUpStatusBarBeVisibleFalse_clockNotDisabled() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mHeadsUpAppearanceController.shouldHeadsUpStatusBarBeVisible()).thenReturn(false); @@ -971,6 +1003,18 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + public void disable_shouldHeadsUpStatusBarBeVisibleTrue_butNoHunBehaviorOn_clockNotDisabled() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + when(mHeadsUpAppearanceController.shouldHeadsUpStatusBarBeVisible()).thenReturn(true); + + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + assertEquals(View.VISIBLE, getClockView().getVisibility()); + } + + @Test public void setUp_fragmentCreatesDaggerComponent() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 7275881e9661..875b655fe3d2 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -35,8 +35,6 @@ import static android.accessibilityservice.AccessibilityTrace.FLAGS_MAGNIFICATIO import static android.accessibilityservice.AccessibilityTrace.FLAGS_PACKAGE_BROADCAST_RECEIVER; import static android.accessibilityservice.AccessibilityTrace.FLAGS_USER_BROADCAST_RECEIVER; import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL; -import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED; -import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; @@ -1116,22 +1114,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mContext.registerReceiverAsUser( receiver, UserHandle.ALL, filter, null, mMainHandler, Context.RECEIVER_EXPORTED); - - if (!android.companion.virtual.flags.Flags.vdmPublicApis()) { - final BroadcastReceiver virtualDeviceReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final int deviceId = intent.getIntExtra( - EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_DEFAULT); - mProxyManager.clearConnections(deviceId); - } - }; - - final IntentFilter virtualDeviceFilter = new IntentFilter( - ACTION_VIRTUAL_DEVICE_REMOVED); - mContext.registerReceiver(virtualDeviceReceiver, virtualDeviceFilter, - Context.RECEIVER_NOT_EXPORTED); - } } /** diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java index f8551457d04d..8adee24c7143 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java @@ -217,7 +217,7 @@ public class ProxyManager { private void registerVirtualDeviceListener() { VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class); - if (vdm == null || !android.companion.virtual.flags.Flags.vdmPublicApis()) { + if (vdm == null) { return; } if (mVirtualDeviceListener == null) { @@ -234,7 +234,7 @@ public class ProxyManager { private void unregisterVirtualDeviceListener() { VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class); - if (vdm == null || !android.companion.virtual.flags.Flags.vdmPublicApis()) { + if (vdm == null) { return; } vdm.unregisterVirtualDeviceListener(mVirtualDeviceListener); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 5edd9d7041ba..f401e6b66093 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -482,14 +482,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } mVirtualDeviceLog.logCreated(deviceId, mOwnerUid); - if (Flags.vdmPublicApis()) { - mPublicVirtualDeviceObject = new VirtualDevice( - this, getDeviceId(), getPersistentDeviceId(), mParams.getName(), - getDisplayName()); - } else { - mPublicVirtualDeviceObject = new VirtualDevice( - this, getDeviceId(), getPersistentDeviceId(), mParams.getName()); - } + mPublicVirtualDeviceObject = new VirtualDevice( + this, getDeviceId(), getPersistentDeviceId(), mParams.getName(), getDisplayName()); mActivityPolicyExemptions = new ArraySet<>( mParams.getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT @@ -1357,10 +1351,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } private boolean hasCustomAudioInputSupportInternal() { - if (!Flags.vdmPublicApis()) { - return false; - } - if (!android.media.audiopolicy.Flags.audioMixTestApi()) { return false; } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index a60fa693350c..0b335d318d64 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -318,16 +318,14 @@ public class VirtualDeviceManagerService extends SystemService { mVirtualDevices.remove(deviceId); } - if (Flags.vdmPublicApis()) { - mVirtualDeviceListeners.broadcast(listener -> { - try { - listener.onVirtualDeviceClosed(deviceId); - } catch (RemoteException e) { - Slog.i(TAG, "Failed to invoke onVirtualDeviceClosed listener: " - + e.getMessage()); - } - }); - } + mVirtualDeviceListeners.broadcast(listener -> { + try { + listener.onVirtualDeviceClosed(deviceId); + } catch (RemoteException e) { + Slog.i(TAG, "Failed to invoke onVirtualDeviceClosed listener: " + + e.getMessage()); + } + }); Intent i = new Intent(VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED); i.putExtra(VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, deviceId); @@ -498,16 +496,14 @@ public class VirtualDeviceManagerService extends SystemService { mVirtualDevices.put(deviceId, virtualDevice); } - if (Flags.vdmPublicApis()) { - mVirtualDeviceListeners.broadcast(listener -> { - try { - listener.onVirtualDeviceCreated(deviceId); - } catch (RemoteException e) { - Slog.i(TAG, "Failed to invoke onVirtualDeviceCreated listener: " - + e.getMessage()); - } - }); - } + mVirtualDeviceListeners.broadcast(listener -> { + try { + listener.onVirtualDeviceCreated(deviceId); + } catch (RemoteException e) { + Slog.i(TAG, "Failed to invoke onVirtualDeviceCreated listener: " + + e.getMessage()); + } + }); Counter.logIncrementWithUid( "virtual_devices.value_virtual_devices_created_with_uid_count", attributionSource.getUid()); diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 78f71877afed..6122fdaafe77 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -17,8 +17,6 @@ package com.android.server.clipboard; import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; -import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED; -import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.content.Context.DEVICE_ID_DEFAULT; @@ -46,7 +44,6 @@ import android.content.Context; import android.content.IClipboard; import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; @@ -219,35 +216,7 @@ public class ClipboardService extends SystemService { @Override public void onStart() { publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl()); - if (!android.companion.virtual.flags.Flags.vdmPublicApis() && mVdmInternal != null) { - registerVirtualDeviceBroadcastReceiver(); - } else if (android.companion.virtual.flags.Flags.vdmPublicApis() && mVdm != null) { - registerVirtualDeviceListener(); - } - } - - private void registerVirtualDeviceBroadcastReceiver() { - if (mVirtualDeviceRemovedReceiver != null) { - return; - } - mVirtualDeviceRemovedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (!intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) { - return; - } - final int removedDeviceId = - intent.getIntExtra(EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_INVALID); - synchronized (mLock) { - for (int i = mClipboards.numMaps() - 1; i >= 0; i--) { - mClipboards.delete(mClipboards.keyAt(i), removedDeviceId); - } - } - } - }; - IntentFilter filter = new IntentFilter(ACTION_VIRTUAL_DEVICE_REMOVED); - getContext().registerReceiver(mVirtualDeviceRemovedReceiver, filter, - Context.RECEIVER_NOT_EXPORTED); + registerVirtualDeviceListener(); } private void registerVirtualDeviceListener() { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java index f371823473ef..1a974458403f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java @@ -425,7 +425,6 @@ public class ProxyManagerTest { @Test public void testRegisterProxy_registersVirtualDeviceListener() throws RemoteException { - mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); registerProxy(DISPLAY_ID); verify(mMockIVirtualDeviceManager, times(1)).registerVirtualDeviceListener(any()); @@ -434,7 +433,6 @@ public class ProxyManagerTest { @Test public void testRegisterMultipleProxies_registersOneVirtualDeviceListener() throws RemoteException { - mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); registerProxy(DISPLAY_ID); registerProxy(DISPLAY_2_ID); @@ -443,7 +441,6 @@ public class ProxyManagerTest { @Test public void testUnregisterProxy_unregistersVirtualDeviceListener() throws RemoteException { - mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); registerProxy(DISPLAY_ID); mProxyManager.unregisterProxy(DISPLAY_ID); @@ -454,7 +451,6 @@ public class ProxyManagerTest { @Test public void testUnregisterProxy_onlyUnregistersVirtualDeviceListenerOnLastProxyRemoval() throws RemoteException { - mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); registerProxy(DISPLAY_ID); registerProxy(DISPLAY_2_ID); @@ -468,7 +464,6 @@ public class ProxyManagerTest { @Test public void testRegisteredProxy_virtualDeviceClosed_proxyClosed() throws RemoteException { - mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); registerProxy(DISPLAY_ID); assertThat(mProxyManager.isProxyedDeviceId(DEVICE_ID)).isTrue(); @@ -490,7 +485,6 @@ public class ProxyManagerTest { @Test public void testRegisteredProxy_unrelatedVirtualDeviceClosed_proxyNotClosed() throws RemoteException { - mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); registerProxy(DISPLAY_ID); assertThat(mProxyManager.isProxyedDeviceId(DEVICE_ID)).isTrue(); @@ -507,17 +501,6 @@ public class ProxyManagerTest { assertThat(mProxyManager.isProxyedDisplay(DISPLAY_ID)).isTrue(); } - @Test - public void testRegisterProxy_doesNotRegisterVirtualDeviceListener_flagDisabled() - throws RemoteException { - mSetFlagsRule.disableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS); - registerProxy(DISPLAY_ID); - mProxyManager.unregisterProxy(DISPLAY_ID); - - verify(mMockIVirtualDeviceManager, never()).registerVirtualDeviceListener(any()); - verify(mMockIVirtualDeviceManager, never()).unregisterVirtualDeviceListener(any()); - } - private void registerProxy(int displayId) { try { mProxyManager.registerProxy(mMockAccessibilityServiceClient, displayId, anyInt(), diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java index 4d1d17f184d1..77c2447fc55f 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.when; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.VirtualDevice; -import android.companion.virtual.flags.Flags; import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; @@ -109,8 +108,6 @@ public class VirtualDeviceTest { @Test public void virtualDevice_getDisplayIds() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); - VirtualDevice virtualDevice = new VirtualDevice( mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); @@ -125,8 +122,6 @@ public class VirtualDeviceTest { @Test public void virtualDevice_hasCustomSensorSupport() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); - VirtualDevice virtualDevice = new VirtualDevice( mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); @@ -140,7 +135,6 @@ public class VirtualDeviceTest { @Test public void virtualDevice_hasCustomAudioInputSupport() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); mSetFlagsRule.enableFlags(android.media.audiopolicy.Flags.FLAG_AUDIO_MIX_TEST_API); VirtualDevice virtualDevice = @@ -160,8 +154,6 @@ public class VirtualDeviceTest { @Test public void virtualDevice_hasCustomCameraSupport() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); - VirtualDevice virtualDevice = new VirtualDevice( mVirtualDevice, VIRTUAL_DEVICE_ID, /*persistentId=*/null, /*name=*/null); |