diff options
71 files changed, 3075 insertions, 541 deletions
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index 7724c2369954..92543b1c7646 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -32,6 +32,11 @@ public class AppCompatTaskInfo implements Parcelable { public boolean topActivityEligibleForLetterboxEducation; /** + * Whether the letterbox education is enabled + */ + public boolean isLetterboxEducationEnabled; + + /** * Whether the direct top activity is in size compat mode on foreground. */ public boolean topActivityInSizeCompat; @@ -178,6 +183,7 @@ public class AppCompatTaskInfo implements Parcelable { == that.topActivityEligibleForUserAspectRatioButton && topActivityEligibleForLetterboxEducation == that.topActivityEligibleForLetterboxEducation + && isLetterboxEducationEnabled == that.isLetterboxEducationEnabled && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition && topActivityLetterboxHorizontalPosition == that.topActivityLetterboxHorizontalPosition @@ -192,6 +198,7 @@ public class AppCompatTaskInfo implements Parcelable { * Reads the AppCompatTaskInfo from a parcel. */ void readFromParcel(Parcel source) { + isLetterboxEducationEnabled = source.readBoolean(); topActivityInSizeCompat = source.readBoolean(); topActivityEligibleForLetterboxEducation = source.readBoolean(); isLetterboxDoubleTapEnabled = source.readBoolean(); @@ -212,6 +219,7 @@ public class AppCompatTaskInfo implements Parcelable { */ @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(isLetterboxEducationEnabled); dest.writeBoolean(topActivityInSizeCompat); dest.writeBoolean(topActivityEligibleForLetterboxEducation); dest.writeBoolean(isLetterboxDoubleTapEnabled); @@ -232,6 +240,7 @@ public class AppCompatTaskInfo implements Parcelable { return "AppCompatTaskInfo { topActivityInSizeCompat=" + topActivityInSizeCompat + " topActivityEligibleForLetterboxEducation= " + topActivityEligibleForLetterboxEducation + + "isLetterboxEducationEnabled= " + isLetterboxEducationEnabled + " isLetterboxDoubleTapEnabled= " + isLetterboxDoubleTapEnabled + " topActivityEligibleForUserAspectRatioButton= " + topActivityEligibleForUserAspectRatioButton diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 2d0f6fccb8f2..54f69099e081 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -482,7 +482,8 @@ public class AppOpsManager { UID_STATE_FOREGROUND_SERVICE, UID_STATE_FOREGROUND, UID_STATE_BACKGROUND, - UID_STATE_CACHED + UID_STATE_CACHED, + UID_STATE_NONEXISTENT }) public @interface UidState {} @@ -566,6 +567,12 @@ public class AppOpsManager { public static final int MIN_PRIORITY_UID_STATE = UID_STATE_CACHED; /** + * Special uid state: The UID is not running + * @hide + */ + public static final int UID_STATE_NONEXISTENT = Integer.MAX_VALUE; + + /** * Resolves the first unrestricted state given an app op. * @param op The op to resolve. * @return The last restricted UID state. @@ -596,6 +603,7 @@ public class AppOpsManager { UID_STATE_FOREGROUND, UID_STATE_BACKGROUND, UID_STATE_CACHED + // UID_STATE_NONEXISTENT isn't a real UID state, so it is excluded }; /** @hide */ @@ -615,6 +623,8 @@ public class AppOpsManager { return "bg"; case UID_STATE_CACHED: return "cch"; + case UID_STATE_NONEXISTENT: + return "gone"; default: return "unknown"; } diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 0e285601b76f..2ca58d16eaae 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -166,6 +166,16 @@ flag { } flag { + name: "finish_running_ops_for_killed_packages" + namespace: "permissions" + description: "Finish all appops for a dead app process" + bug: "234630570" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "runtime_permission_appops_mapping_enabled" is_fixed_read_only: true namespace: "permissions" diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java index 29a6db6a12a0..8237b20260ea 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java @@ -105,6 +105,21 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService"; + // TODO(339594686): make API + /** + * @hide + */ + public static final String REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY = + "register_model_update_callback"; + /** + * @hide + */ + public static final String MODEL_LOADED_BUNDLE_KEY = "model_loaded"; + /** + * @hide + */ + public static final String MODEL_UNLOADED_BUNDLE_KEY = "model_unloaded"; + private IRemoteStorageService mRemoteStorageService; /** diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java index 64fe66e36bc3..ec4e3e9163fa 100644 --- a/core/java/android/window/TransitionFilter.java +++ b/core/java/android/window/TransitionFilter.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.WindowConfiguration; import android.content.ComponentName; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.view.WindowManager; @@ -180,6 +181,7 @@ public final class TransitionFilter implements Parcelable { public @ContainerOrder int mOrder = CONTAINER_ORDER_ANY; public ComponentName mTopActivity; + public IBinder mLaunchCookie; public Requirement() { } @@ -193,6 +195,7 @@ public final class TransitionFilter implements Parcelable { mMustBeTask = in.readBoolean(); mOrder = in.readInt(); mTopActivity = in.readTypedObject(ComponentName.CREATOR); + mLaunchCookie = in.readStrongBinder(); } /** Go through changes and find if at-least one change matches this filter */ @@ -231,6 +234,9 @@ public final class TransitionFilter implements Parcelable { if (mMustBeTask && change.getTaskInfo() == null) { continue; } + if (!matchesCookie(change.getTaskInfo())) { + continue; + } return true; } return false; @@ -247,13 +253,25 @@ public final class TransitionFilter implements Parcelable { return false; } + private boolean matchesCookie(ActivityManager.RunningTaskInfo info) { + if (mLaunchCookie == null) return true; + if (info == null) return false; + for (IBinder cookie : info.launchCookies) { + if (mLaunchCookie.equals(cookie)) { + return true; + } + } + return false; + } + /** Check if the request matches this filter. It may generate false positives */ boolean matches(@NonNull TransitionRequestInfo request) { // Can't check modes/order since the transition hasn't been built at this point. if (mActivityType == ACTIVITY_TYPE_UNDEFINED) return true; return request.getTriggerTask() != null && request.getTriggerTask().getActivityType() == mActivityType - && matchesTopActivity(request.getTriggerTask(), null /* activityCmp */); + && matchesTopActivity(request.getTriggerTask(), null /* activityCmp */) + && matchesCookie(request.getTriggerTask()); } @Override @@ -267,6 +285,7 @@ public final class TransitionFilter implements Parcelable { dest.writeBoolean(mMustBeTask); dest.writeInt(mOrder); dest.writeTypedObject(mTopActivity, flags); + dest.writeStrongBinder(mLaunchCookie); } @NonNull @@ -307,6 +326,7 @@ public final class TransitionFilter implements Parcelable { out.append(" mustBeTask=" + mMustBeTask); out.append(" order=" + containerOrderToString(mOrder)); out.append(" topActivity=").append(mTopActivity); + out.append(" launchCookie=").append(mLaunchCookie); out.append("}"); return out.toString(); } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 37771a2a3a24..5bd20332f381 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4717,6 +4717,13 @@ <!-- The component name for the default system on-device sandboxed inference service. --> <string name="config_defaultOnDeviceSandboxedInferenceService" translatable="false"></string> + <!-- The broadcast intent name for notifying when the on-device model is loading --> + <string name="config_onDeviceIntelligenceModelLoadedBroadcastKey" translatable="false"></string> + + <!-- The broadcast intent name for notifying when the on-device model has been unloaded --> + <string name="config_onDeviceIntelligenceModelUnloadedBroadcastKey" translatable="false"></string> + + <!-- Component name that accepts ACTION_SEND intents for requesting ambient context consent for wearable sensing. --> <string translatable="false" name="config_defaultWearableSensingConsentComponent"></string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e5768e4a1def..ae79a4c68f9e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3943,6 +3943,8 @@ <java-symbol type="string" name="config_defaultWearableSensingService" /> <java-symbol type="string" name="config_defaultOnDeviceIntelligenceService" /> <java-symbol type="string" name="config_defaultOnDeviceSandboxedInferenceService" /> + <java-symbol type="string" name="config_onDeviceIntelligenceModelLoadedBroadcastKey" /> + <java-symbol type="string" name="config_onDeviceIntelligenceModelUnloadedBroadcastKey" /> <java-symbol type="string" name="config_retailDemoPackage" /> <java-symbol type="string" name="config_retailDemoPackageSignature" /> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 5c292f173e5b..bfac24b81d2f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -188,6 +188,11 @@ public class CompatUIController implements OnDisplaysChangedListener, */ private boolean mHasShownUserAspectRatioSettingsButton = false; + /** + * This is true when the rechability education is displayed for the first time. + */ + private boolean mIsFirstReachabilityEducationRunning; + public CompatUIController(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -252,9 +257,35 @@ public class CompatUIController implements OnDisplaysChangedListener, removeLayouts(taskInfo.taskId); return; } - + // We're showing the first reachability education so we ignore incoming TaskInfo + // until the education flow has completed or we double tap. + if (mIsFirstReachabilityEducationRunning) { + return; + } + if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { + if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) { + createOrUpdateLetterboxEduLayout(taskInfo, taskListener); + } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) { + // In this case the app is letterboxed and the letterbox education + // is disabled. In this case we need to understand if it's the first + // time we show the reachability education. When this is happening + // we need to ignore all the incoming TaskInfo until the education + // completes. If we come from a double tap we follow the normal flow. + final boolean topActivityPillarboxed = + taskInfo.appCompatTaskInfo.isTopActivityPillarboxed(); + final boolean isFirstTimeHorizontalReachabilityEdu = topActivityPillarboxed + && !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(taskInfo); + final boolean isFirstTimeVerticalReachabilityEdu = !topActivityPillarboxed + && !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(taskInfo); + if (isFirstTimeHorizontalReachabilityEdu || isFirstTimeVerticalReachabilityEdu) { + mIsFirstReachabilityEducationRunning = true; + mCompatUIConfiguration.setSeenLetterboxEducation(taskInfo.userId); + createOrUpdateReachabilityEduLayout(taskInfo, taskListener); + return; + } + } + } createOrUpdateCompatLayout(taskInfo, taskListener); - createOrUpdateLetterboxEduLayout(taskInfo, taskListener); createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { createOrUpdateReachabilityEduLayout(taskInfo, taskListener); @@ -589,6 +620,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { // We need to update the UI otherwise it will not be shown until the user relaunches the app + mIsFirstReachabilityEducationRunning = false; createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index b41454d932a5..5af4c3b0a716 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -41,15 +41,6 @@ public class DesktopModeStatus { public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean( "persist.wm.debug.desktop_change_display", false); - - /** - * Flag to indicate that desktop stashing is enabled. - * When enabled, swiping home from desktop stashes the open apps. Next app that launches, - * will be added to the desktop. - */ - private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean( - "persist.wm.debug.desktop_stashing", false); - /** * Flag to indicate whether to apply shadows to windows in desktop mode. */ @@ -109,14 +100,6 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop task stashing is enabled when going home. - * Allows users to use home screen to add tasks to desktop. - */ - public static boolean isStashingEnabled() { - return IS_STASHING_ENABLED; - } - - /** * Return whether to use window shadows. * * @param isFocusedWindow whether the window to apply shadows to is focused diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 2d508b2e6e3d..6bbc8fec2894 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -48,7 +48,6 @@ class DesktopModeTaskRepository { val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), val minimizedTasks: ArraySet<Int> = ArraySet(), - var stashed: Boolean = false ) // Token of the current wallpaper activity, used to remove it when the last task is removed @@ -95,10 +94,8 @@ class DesktopModeTaskRepository { visibleTasksListeners[visibleTasksListener] = executor displayData.keyIterator().forEach { displayId -> val visibleTasksCount = getVisibleTaskCount(displayId) - val stashed = isStashed(displayId) executor.execute { visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount) - visibleTasksListener.onStashedChanged(displayId, stashed) } } } @@ -400,26 +397,6 @@ class DesktopModeTaskRepository { } /** - * Update stashed status on display with id [displayId] - */ - fun setStashed(displayId: Int, stashed: Boolean) { - val data = displayData.getOrCreate(displayId) - val oldValue = data.stashed - data.stashed = stashed - if (oldValue != stashed) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: mark stashed=%b displayId=%d", - stashed, - displayId - ) - visibleTasksListeners.forEach { (listener, executor) -> - executor.execute { listener.onStashedChanged(displayId, stashed) } - } - } - } - - /** * Removes and returns the bounds saved before maximizing the given task. */ fun removeBoundsBeforeMaximize(taskId: Int): Rect? { @@ -433,13 +410,6 @@ class DesktopModeTaskRepository { boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) } - /** - * Check if display with id [displayId] has desktop tasks stashed - */ - fun isStashed(displayId: Int): Boolean { - return displayData[displayId]?.stashed ?: false - } - internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") @@ -455,7 +425,6 @@ class DesktopModeTaskRepository { pw.println("${prefix}Display $displayId:") pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}") pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}") - pw.println("${innerPrefix}stashed=${data.stashed}") } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index b0d59231500b..b2bdbfefb9aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -240,34 +240,6 @@ class DesktopTasksController( } } - /** - * Stash desktop tasks on display with id [displayId]. - * - * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps - * launched in this state will be added to the desktop. Existing desktop tasks will be brought - * back to front during the launch. - */ - fun stashDesktopApps(displayId: Int) { - if (DesktopModeStatus.isStashingEnabled()) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps") - desktopModeTaskRepository.setStashed(displayId, true) - } - } - - /** - * Clear the stashed state for the given display - */ - fun hideStashedDesktopApps(displayId: Int) { - if (DesktopModeStatus.isStashingEnabled()) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: hideStashedApps displayId=%d", - displayId - ) - desktopModeTaskRepository.setStashed(displayId, false) - } - } - /** Get number of tasks that are marked as visible */ fun getVisibleTaskCount(displayId: Int): Int { return desktopModeTaskRepository.getVisibleTaskCount(displayId) @@ -871,8 +843,6 @@ class DesktopTasksController( val result = triggerTask?.let { task -> when { request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) - // If display has tasks stashed, handle as stashed launch - task.isStashed -> handleStashedTaskLaunch(task, transition) // Check if the task has a top transparent activity shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task) // Check if fullscreen task should be updated @@ -911,12 +881,8 @@ class DesktopTasksController( .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } } - private val TaskInfo.isStashed: Boolean - get() = desktopModeTaskRepository.isStashed(displayId) - - private fun shouldLaunchAsModal(task: TaskInfo): Boolean { - return Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) - } + private fun shouldLaunchAsModal(task: TaskInfo) = + Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean { return Flags.enableDesktopWindowingWallpaperActivity() && @@ -976,24 +942,6 @@ class DesktopTasksController( return null } - private fun handleStashedTaskLaunch( - task: RunningTaskInfo, - transition: IBinder - ): WindowContainerTransaction { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: launch apps with stashed on transition taskId=%d", - task.taskId - ) - val wct = WindowContainerTransaction() - val taskToMinimize = - bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) - addMoveToDesktopChanges(wct, task) - desktopModeTaskRepository.setStashed(task.displayId, false) - addPendingMinimizeTransition(transition, taskToMinimize) - return wct - } - // Always launch transparent tasks in fullscreen. private fun handleTransparentTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { // Already fullscreen, no-op. @@ -1467,25 +1415,25 @@ class DesktopTasksController( ) { c -> c.showDesktopApps(displayId, remoteTransition) } } - override fun stashDesktopApps(displayId: Int) { + override fun showDesktopApp(taskId: Int) { ExecutorUtils.executeRemoteCallWithTaskPermission( controller, - "stashDesktopApps" - ) { c -> c.stashDesktopApps(displayId) } + "showDesktopApp" + ) { c -> c.moveTaskToFront(taskId) } } - override fun hideStashedDesktopApps(displayId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "hideStashedDesktopApps" - ) { c -> c.hideStashedDesktopApps(displayId) } + override fun stashDesktopApps(displayId: Int) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: stashDesktopApps is deprecated" + ) } - override fun showDesktopApp(taskId: Int) { - ExecutorUtils.executeRemoteCallWithTaskPermission( - controller, - "showDesktopApp" - ) { c -> c.moveTaskToFront(taskId) } + override fun hideStashedDesktopApps(displayId: Int) { + KtProtoLog.w( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: hideStashedDesktopApps is deprecated" + ) } override fun getVisibleTaskCount(displayId: Int): Int { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index fa4352241193..c36f8deb6ecc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -28,10 +28,10 @@ interface IDesktopMode { /** Show apps on the desktop on the given display */ void showDesktopApps(int displayId, in RemoteTransition remoteTransition); - /** Stash apps on the desktop to allow launching another app from home screen */ + /** @deprecated use {@link #showDesktopApps} instead. */ void stashDesktopApps(int displayId); - /** Hide apps that may be stashed */ + /** @deprecated this is no longer supported. */ void hideStashedDesktopApps(int displayId); /** Bring task with the given id to front */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index afae653f0682..9c008647104a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -668,6 +668,18 @@ public class CompatUIControllerTest extends ShellTestCase { Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton()); } + @Test + public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false; + + mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + + verify(mController, never()).createLetterboxEduWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); + } + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, @CameraCompatControlState int cameraCompatControlState) { return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState, @@ -694,6 +706,8 @@ public class CompatUIControllerTest extends ShellTestCase { taskInfo.isVisible = isVisible; taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; + taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = true; + taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = true; return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index dca7be12fffc..8f59f30da697 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -182,18 +182,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun addListener_notifiesStashed() { - repo.setStashed(DEFAULT_DISPLAY, true) - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - executor.flushAll() - - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) - } - - @Test fun addListener_tasksOnDifferentDisplay_doesNotNotify() { repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true) val listener = TestVisibilityListener() @@ -400,65 +388,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun setStashed_stateIsUpdatedForTheDisplay() { - repo.setStashed(DEFAULT_DISPLAY, true) - assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue() - assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse() - - repo.setStashed(DEFAULT_DISPLAY, false) - assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse() - } - - @Test - fun setStashed_notifyListener() { - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - repo.setStashed(DEFAULT_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) - - repo.setStashed(DEFAULT_DISPLAY, false) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isFalse() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2) - } - - @Test - fun setStashed_secondCallDoesNotNotify() { - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - repo.setStashed(DEFAULT_DISPLAY, true) - repo.setStashed(DEFAULT_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) - } - - @Test - fun setStashed_tracksPerDisplay() { - val listener = TestVisibilityListener() - val executor = TestShellExecutor() - repo.addVisibleTasksListener(listener, executor) - - repo.setStashed(DEFAULT_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedOnSecondaryDisplay).isFalse() - - repo.setStashed(SECOND_DISPLAY, true) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isTrue() - assertThat(listener.stashedOnSecondaryDisplay).isTrue() - - repo.setStashed(DEFAULT_DISPLAY, false) - executor.flushAll() - assertThat(listener.stashedOnDefaultDisplay).isFalse() - assertThat(listener.stashedOnSecondaryDisplay).isTrue() - } - - @Test fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) @@ -598,12 +527,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 - var stashedOnDefaultDisplay = false - var stashedOnSecondaryDisplay = false - - var stashedChangesOnDefaultDisplay = 0 - var stashedChangesOnSecondaryDisplay = 0 - override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { when (displayId) { DEFAULT_DISPLAY -> { @@ -617,20 +540,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { else -> fail("Visible task listener received unexpected display id: $displayId") } } - - override fun onStashedChanged(displayId: Int, stashed: Boolean) { - when (displayId) { - DEFAULT_DISPLAY -> { - stashedOnDefaultDisplay = stashed - stashedChangesOnDefaultDisplay++ - } - SECOND_DISPLAY -> { - stashedOnSecondaryDisplay = stashed - stashedChangesOnDefaultDisplay++ - } - else -> fail("Visible task listener received unexpected display id: $displayId") - } - } } companion object { 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 3f76c4f556f7..7e55628b5641 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 @@ -1044,29 +1044,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) - markTaskHidden(stashedFreeformTask) - - val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY) - - controller.stashDesktopApps(DEFAULT_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - assertThat(result).isNotNull() - result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask) - assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - - // Stashed state should be cleared - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() - } - - @Test fun handleRequest_freeformTask_freeformVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1133,27 +1110,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) - markTaskHidden(stashedFreeformTask) - - val freeformTask = createFreeformTask(DEFAULT_DISPLAY) - - controller.stashDesktopApps(DEFAULT_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(freeformTask)) - assertThat(result).isNotNull() - result?.assertReorderSequence(stashedFreeformTask, freeformTask) - - // Stashed state should be cleared - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() - } - - @Test fun handleRequest_notOpenOrToFrontTransition_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1269,29 +1225,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun stashDesktopApps_stateUpdates() { - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - controller.stashDesktopApps(DEFAULT_DISPLAY) - - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue() - assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse() - } - - @Test - fun hideStashedDesktopApps_stateUpdates() { - whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) - - desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true) - desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true) - controller.hideStashedDesktopApps(DEFAULT_DISPLAY) - - assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() - // Check that second display is not affected - assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue() - } - - @Test fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { val task = setUpFreeformTask() clearInvocations(launchAdjacentController) diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java index 25ac3c9d9074..635dc420f18c 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java @@ -172,7 +172,7 @@ public class DynamicSystemInstallationService extends Service // This is for testing only now private boolean mEnableWhenCompleted; - private boolean mOneShot; + private boolean mOneShot = true; private boolean mHideNotification; private InstallationAsyncTask.Progress mInstallTaskProgress; diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index d4660fa74f0a..23df26fdb246 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -23,6 +23,7 @@ import android.app.TaskInfo import android.graphics.Matrix import android.graphics.Rect import android.graphics.RectF +import android.os.Binder import android.os.Build import android.os.Handler import android.os.Looper @@ -36,7 +37,11 @@ import android.view.SyncRtSurfaceTransactionApplier import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.animation.PathInterpolator +import android.window.RemoteTransition +import android.window.TransitionFilter import androidx.annotation.AnyThread import androidx.annotation.BinderThread import androidx.annotation.UiThread @@ -44,6 +49,9 @@ import com.android.app.animation.Interpolators import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.ScreenDecorationsUtils import com.android.systemui.Flags.activityTransitionUseLargestWindow +import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary +import com.android.wm.shell.shared.IShellTransitions +import com.android.wm.shell.shared.ShellTransitions import java.util.concurrent.Executor import kotlin.math.roundToInt @@ -59,6 +67,9 @@ constructor( /** The executor that runs on the main thread. */ private val mainExecutor: Executor, + /** The object used to register ephemeral returns and long-lived transitions. */ + private val transitionRegister: TransitionRegister? = null, + /** The animator used when animating a View into an app. */ private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor), @@ -74,6 +85,36 @@ constructor( // TODO(b/301385865): Remove this flag. private val disableWmTimeout: Boolean = false, ) { + @JvmOverloads + constructor( + mainExecutor: Executor, + shellTransitions: ShellTransitions, + transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor), + dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor), + disableWmTimeout: Boolean = false, + ) : this( + mainExecutor, + TransitionRegister.fromShellTransitions(shellTransitions), + transitionAnimator, + dialogToAppAnimator, + disableWmTimeout, + ) + + @JvmOverloads + constructor( + mainExecutor: Executor, + iShellTransitions: IShellTransitions, + transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor), + dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor), + disableWmTimeout: Boolean = false, + ) : this( + mainExecutor, + TransitionRegister.fromIShellTransitions(iShellTransitions), + transitionAnimator, + dialogToAppAnimator, + disableWmTimeout, + ) + companion object { /** The timings when animating a View into an app. */ @JvmField @@ -233,6 +274,10 @@ constructor( } } + if (animationAdapter != null && controller.transitionCookie != null) { + registerEphemeralReturnAnimation(controller, transitionRegister) + } + val launchResult = intentStarter(animationAdapter) // Only animate if the app is not already on top and will be opened, unless we are on the @@ -302,6 +347,66 @@ constructor( } } + /** + * Uses [transitionRegister] to set up the return animation for the given [launchController]. + * + * De-registration is set up automatically once the return animation is run. + * + * TODO(b/339194555): automatically de-register when the launchable is detached. + */ + private fun registerEphemeralReturnAnimation( + launchController: Controller, + transitionRegister: TransitionRegister? + ) { + if (!returnAnimationFrameworkLibrary()) return + + var cleanUpRunnable: Runnable? = null + val returnRunner = + createRunner( + object : DelegateTransitionAnimatorController(launchController) { + override val isLaunching = false + + override fun onTransitionAnimationCancelled( + newKeyguardOccludedState: Boolean? + ) { + super.onTransitionAnimationCancelled(newKeyguardOccludedState) + cleanUp() + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + super.onTransitionAnimationEnd(isExpandingFullyAbove) + cleanUp() + } + + private fun cleanUp() { + cleanUpRunnable?.run() + } + } + ) + + // mTypeSet and mModes match back signals only, and not home. This is on purpose, because + // we only want ephemeral return animations triggered in these scenarios. + val filter = + TransitionFilter().apply { + mTypeSet = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK) + mRequirements = + arrayOf( + TransitionFilter.Requirement().apply { + mLaunchCookie = launchController.transitionCookie + mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK) + } + ) + } + val transition = + RemoteTransition( + RemoteAnimationRunnerCompat.wrap(returnRunner), + "${launchController.transitionCookie}_returnTransition" + ) + + transitionRegister?.register(filter, transition) + cleanUpRunnable = Runnable { transitionRegister?.unregister(transition) } + } + /** Add a [Listener] that can listen to transition animations. */ fun addListener(listener: Listener) { listeners.add(listener) @@ -386,8 +491,14 @@ constructor( * Note: The background of [view] should be a (rounded) rectangle so that it can be * properly animated. */ + @JvmOverloads @JvmStatic - fun fromView(view: View, cujType: Int? = null): Controller? { + fun fromView( + view: View, + cujType: Int? = null, + cookie: TransitionCookie? = null, + returnCujType: Int? = null + ): Controller? { // Make sure the View we launch from implements LaunchableView to avoid visibility // issues. if (view !is LaunchableView) { @@ -408,7 +519,7 @@ constructor( return null } - return GhostedViewTransitionAnimatorController(view, cujType) + return GhostedViewTransitionAnimatorController(view, cujType, cookie, returnCujType) } } @@ -432,6 +543,17 @@ constructor( get() = false /** + * The cookie associated with the transition controlled by this [Controller]. + * + * This should be defined for all return [Controller] (when [isLaunching] is false) and for + * their associated launch [Controller]s. + * + * For the recommended format, see [TransitionCookie]. + */ + val transitionCookie: TransitionCookie? + get() = null + + /** * The intent was started. If [willAnimate] is false, nothing else will happen and the * animation will not be started. */ @@ -652,7 +774,7 @@ constructor( return } - val window = findRootTaskIfPossible(apps) + val window = findTargetWindowIfPossible(apps) if (window == null) { Log.i(TAG, "Aborting the animation as no window is opening") callback?.invoke() @@ -676,7 +798,7 @@ constructor( startAnimation(window, navigationBar, callback) } - private fun findRootTaskIfPossible( + private fun findTargetWindowIfPossible( apps: Array<out RemoteAnimationTarget>? ): RemoteAnimationTarget? { if (apps == null) { @@ -694,6 +816,19 @@ constructor( for (it in apps) { if (it.mode == targetMode) { if (activityTransitionUseLargestWindow()) { + if (returnAnimationFrameworkLibrary()) { + // If the controller contains a cookie, _only_ match if the candidate + // contains the matching cookie. + if ( + controller.transitionCookie != null && + it.taskInfo + ?.launchCookies + ?.contains(controller.transitionCookie) != true + ) { + continue + } + } + if ( candidate == null || !it.hasAnimatingParent && candidate.hasAnimatingParent @@ -806,11 +941,7 @@ constructor( progress: Float, linearProgress: Float ) { - // Apply the state to the window only if it is visible, i.e. when the - // expanding view is *not* visible. - if (!state.visible) { - applyStateToWindow(window, state, linearProgress) - } + applyStateToWindow(window, state, linearProgress) navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } listener?.onTransitionAnimationProgress(linearProgress) @@ -1048,4 +1179,72 @@ constructor( return (this.width() * this.height()) > (other.width() * other.height()) } } + + /** + * Wraps one of the two methods we have to register remote transitions with WM Shell: + * - for in-process registrations (e.g. System UI) we use [ShellTransitions] + * - for cross-process registrations (e.g. Launcher) we use [IShellTransitions] + * + * Important: each instance of this class must wrap exactly one of the two. + */ + class TransitionRegister + private constructor( + private val shellTransitions: ShellTransitions? = null, + private val iShellTransitions: IShellTransitions? = null, + ) { + init { + assert((shellTransitions != null).xor(iShellTransitions != null)) + } + + companion object { + /** Provides a [TransitionRegister] instance wrapping [ShellTransitions]. */ + fun fromShellTransitions(shellTransitions: ShellTransitions): TransitionRegister { + return TransitionRegister(shellTransitions = shellTransitions) + } + + /** Provides a [TransitionRegister] instance wrapping [IShellTransitions]. */ + fun fromIShellTransitions(iShellTransitions: IShellTransitions): TransitionRegister { + return TransitionRegister(iShellTransitions = iShellTransitions) + } + } + + /** Register [remoteTransition] with WM Shell using the given [filter]. */ + internal fun register( + filter: TransitionFilter, + remoteTransition: RemoteTransition, + ) { + shellTransitions?.registerRemote(filter, remoteTransition) + iShellTransitions?.registerRemote(filter, remoteTransition) + } + + /** Unregister [remoteTransition] from WM Shell. */ + internal fun unregister(remoteTransition: RemoteTransition) { + shellTransitions?.unregisterRemote(remoteTransition) + iShellTransitions?.unregisterRemote(remoteTransition) + } + } + + /** + * A cookie used to uniquely identify a task launched using an + * [ActivityTransitionAnimator.Controller]. + * + * The [String] encapsulated by this class should be formatted in such a way to be unique across + * the system, but reliably constant for the same associated launchable. + * + * Recommended naming scheme: + * - DO use the fully qualified name of the class that owns the instance of the launchable, + * along with a concise and precise description of the purpose of the launchable in question. + * - DO NOT introduce uniqueness through the use of timestamps or other runtime variables that + * will change if the instance is destroyed and re-created. + * + * Example: "com.not.the.real.class.name.ShadeController_openSettingsButton" + * + * Note that sometimes (e.g. in recycler views) there could be multiple instances of the same + * launchable, and no static knowledge to adequately differentiate between them using a single + * description. In this case, the recommendation is to append a unique identifier related to the + * contents of the launchable. + * + * Example: “com.not.the.real.class.name.ToastWebResult_launchAga_id143256” + */ + data class TransitionCookie(private val cookie: String) : Binder() } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt index e4bb2adbefb4..21557b8bb402 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -25,10 +25,30 @@ interface Expandable { * [Expandable] into an Activity, or return `null` if this [Expandable] should not be animated * (e.g. if it is currently not attached or visible). * - * @param cujType the CUJ type from the [com.android.internal.jank.InteractionJankMonitor] + * @param launchCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor] * associated to the launch that will use this controller. + * @param cookie The unique cookie associated with the launch that will use this controller. + * This is required iff the a return animation should be included. + * @param returnCujType The CUJ type from the [com.android.internal.jank.InteractionJankMonitor] + * associated to the return animation that will use this controller. */ - fun activityTransitionController(cujType: Int? = null): ActivityTransitionAnimator.Controller? + fun activityTransitionController( + launchCujType: Int? = null, + cookie: ActivityTransitionAnimator.TransitionCookie? = null, + returnCujType: Int? = null + ): ActivityTransitionAnimator.Controller? + + /** + * See [activityTransitionController] above. + * + * Interfaces don't support [JvmOverloads], so this is a useful overload for Java usages that + * don't use the return-related parameters. + */ + fun activityTransitionController( + launchCujType: Int? = null + ): ActivityTransitionAnimator.Controller? { + return activityTransitionController(launchCujType, cookie = null, returnCujType = null) + } /** * Create a [DialogTransitionAnimator.Controller] that can be used to expand this [Expandable] @@ -48,9 +68,16 @@ interface Expandable { fun fromView(view: View): Expandable { return object : Expandable { override fun activityTransitionController( - cujType: Int?, + launchCujType: Int?, + cookie: ActivityTransitionAnimator.TransitionCookie?, + returnCujType: Int? ): ActivityTransitionAnimator.Controller? { - return ActivityTransitionAnimator.Controller.fromView(view, cujType) + return ActivityTransitionAnimator.Controller.fromView( + view, + launchCujType, + cookie, + returnCujType + ) } override fun dialogTransitionController( diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index fd79f62debce..9d4507337e51 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -59,8 +59,12 @@ constructor( /** The view that will be ghosted and from which the background will be extracted. */ private val ghostedView: View, - /** The [CujType] associated to this animation. */ - private val cujType: Int? = null, + /** The [CujType] associated to this launch animation. */ + private val launchCujType: Int? = null, + override val transitionCookie: ActivityTransitionAnimator.TransitionCookie? = null, + + /** The [CujType] associated to this return animation. */ + private val returnCujType: Int? = null, private var interactionJankMonitor: InteractionJankMonitor = InteractionJankMonitor.getInstance(), ) : ActivityTransitionAnimator.Controller { @@ -104,6 +108,15 @@ constructor( */ private val background: Drawable? + /** CUJ identifier accounting for whether this controller is for a launch or a return. */ + private val cujType: Int? + get() = + if (isLaunching) { + launchCujType + } else { + returnCujType + } + init { // Make sure the View we launch from implements LaunchableView to avoid visibility issues. if (ghostedView !is LaunchableView) { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt index c7f0a965206e..17a606171a9e 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt @@ -134,13 +134,15 @@ internal class ExpandableControllerImpl( override val expandable: Expandable = object : Expandable { override fun activityTransitionController( - cujType: Int?, + launchCujType: Int?, + cookie: ActivityTransitionAnimator.TransitionCookie?, + returnCujType: Int? ): ActivityTransitionAnimator.Controller? { if (!isComposed.value) { return null } - return activityController(cujType) + return activityController(launchCujType, cookie, returnCujType) } override fun dialogTransitionController( @@ -262,10 +264,27 @@ internal class ExpandableControllerImpl( } /** Create an [ActivityTransitionAnimator.Controller] that can be used to animate activities. */ - private fun activityController(cujType: Int?): ActivityTransitionAnimator.Controller { + private fun activityController( + launchCujType: Int?, + cookie: ActivityTransitionAnimator.TransitionCookie?, + returnCujType: Int? + ): ActivityTransitionAnimator.Controller { val delegate = transitionController() return object : ActivityTransitionAnimator.Controller, TransitionAnimator.Controller by delegate { + /** + * CUJ identifier accounting for whether this controller is for a launch or a return. + */ + private val cujType: Int? + get() = + if (isLaunching) { + launchCujType + } else { + returnCujType + } + + override val transitionCookie = cookie + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { delegate.onTransitionAnimationStart(isExpandingFullyAbove) overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt index 79d17efcacc1..44b221c1f5e1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.anc.ui.composable +import android.view.Gravity import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -27,9 +28,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics @@ -39,6 +46,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.android.systemui.res.R import com.android.systemui.volume.panel.component.anc.ui.viewmodel.AncViewModel +import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope import javax.inject.Inject @@ -54,15 +62,22 @@ constructor( override fun VolumePanelComposeScope.Content(modifier: Modifier) { val slice by viewModel.buttonSlice.collectAsState() val label = stringResource(R.string.volume_panel_noise_control_title) + val screenWidth: Float = + with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() } + var gravity by remember { mutableIntStateOf(Gravity.CENTER_HORIZONTAL) } val isClickable = viewModel.isClickable(slice) val onClick = if (isClickable) { - { ancPopup.show(null) } + { with(ancPopup) { show(null, gravity) } } } else { null } + Column( - modifier = modifier, + modifier = + modifier.onGloballyPositioned { + gravity = VolumePanelPopup.calculateGravity(it, screenWidth) + }, verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt index e1ee01e78566..d53dbf9ddd48 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.anc.ui.composable +import android.view.Gravity import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme @@ -47,9 +48,10 @@ constructor( ) { /** Shows a popup with the [expandable] animation. */ - fun show(expandable: Expandable?) { + fun show(expandable: Expandable?, horizontalGravity: Int) { uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_ANC_POPUP_SHOWN) - volumePanelPopup.show(expandable, { Title() }, { Content(it) }) + val gravity = horizontalGravity or Gravity.BOTTOM + volumePanelPopup.show(expandable, gravity, { Title() }, { Content(it) }) } @Composable diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt index 0893b9d4c580..f11c3a5852d8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.button.ui.composable +import android.view.Gravity import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -29,8 +30,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -41,6 +48,7 @@ import com.android.compose.animation.Expandable import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.volume.panel.component.button.ui.viewmodel.ButtonViewModel +import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup.Companion.calculateGravity import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope import kotlinx.coroutines.flow.StateFlow @@ -48,7 +56,7 @@ import kotlinx.coroutines.flow.StateFlow /** [ComposeVolumePanelUiComponent] implementing a clickable button from a bottom row. */ class ButtonComponent( private val viewModelFlow: StateFlow<ButtonViewModel?>, - private val onClick: (Expandable) -> Unit + private val onClick: (expandable: Expandable, horizontalGravity: Int) -> Unit ) : ComposeVolumePanelUiComponent { @Composable @@ -57,8 +65,13 @@ class ButtonComponent( val viewModel = viewModelByState ?: return val label = viewModel.label.toString() + val screenWidth: Float = + with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() } + var gravity by remember { mutableIntStateOf(Gravity.CENTER_HORIZONTAL) } + Column( - modifier = modifier, + modifier = + modifier.onGloballyPositioned { gravity = calculateGravity(it, screenWidth) }, verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -82,7 +95,7 @@ class ButtonComponent( } else { MaterialTheme.colorScheme.onSurface }, - onClick = onClick, + onClick = { onClick(it, gravity) }, ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt index bb4e9574c602..3b1bf2ab9dcd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.popup.ui.composable import android.view.Gravity +import androidx.annotation.GravityInt import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,6 +32,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.paneTitle @@ -60,13 +63,14 @@ constructor( */ fun show( expandable: Expandable?, + @GravityInt gravity: Int, title: @Composable (SystemUIDialog) -> Unit, content: @Composable (SystemUIDialog) -> Unit, ) { val dialog = dialogFactory.create( theme = R.style.Theme_VolumePanel_Popup, - dialogGravity = Gravity.BOTTOM, + dialogGravity = gravity, ) { PopupComposable(it, title, content) } @@ -122,4 +126,23 @@ constructor( } } } + + companion object { + + /** + * Returns absolute ([Gravity.LEFT], [Gravity.RIGHT] or [Gravity.CENTER_HORIZONTAL]) + * [GravityInt] for the popup based on the [coordinates] global position relative to the + * [screenWidthPx]. + */ + @GravityInt + fun calculateGravity(coordinates: LayoutCoordinates, screenWidthPx: Float): Int { + val bottomCenter: Float = coordinates.boundsInRoot().bottomCenter.x + val rootBottomCenter: Float = screenWidthPx / 2 + return when { + bottomCenter < rootBottomCenter -> Gravity.LEFT + bottomCenter > rootBottomCenter -> Gravity.RIGHT + else -> Gravity.CENTER_HORIZONTAL + } + } + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt index 12d2bc2e274b..d41acd9e852c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.spatialaudio.ui.composable +import android.view.Gravity import androidx.compose.foundation.basicMarquee import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -47,14 +48,15 @@ constructor( ) { /** Shows a popup with the [expandable] animation. */ - fun show(expandable: Expandable) { + fun show(expandable: Expandable, horizontalGravity: Int) { uiEventLogger.logWithPosition( VolumePanelUiEvent.VOLUME_PANEL_SPATIAL_AUDIO_POP_UP_SHOWN, 0, null, viewModel.spatialAudioButtons.value.indexOfFirst { it.button.isActive } ) - volumePanelPopup.show(expandable, { Title() }, { Content(it) }) + val gravity = horizontalGravity or Gravity.BOTTOM + volumePanelPopup.show(expandable, gravity, { Title() }, { Content(it) }) } @Composable diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 7f3274c09037..7655d7a89dc3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -247,7 +247,7 @@ constructor( state: TransitionState ) { if (updateTransitionId != transitionId) { - Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + Log.w(TAG, "Attempting to update with old/invalid transitionId: $transitionId") return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt new file mode 100644 index 000000000000..80bdc65f9b97 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt @@ -0,0 +1,37 @@ +/* + * 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.systemui.keyguard.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow + +@SysUISingleton +class LockscreenSceneTransitionRepository @Inject constructor() { + + /** + * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the + * next transition into the Lockscreen scene is started. It will be consumed exactly once and + * after that the state will be set back to [DEFAULT_STATE]. + */ + val nextLockscreenTargetState: MutableStateFlow<KeyguardState> = MutableStateFlow(DEFAULT_STATE) + + companion object { + val DEFAULT_STATE = KeyguardState.LOCKSCREEN + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 2c05d49f8040..30c6718adf1b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -17,6 +17,8 @@ package com.android.systemui.keyguard.domain.interactor +import android.annotation.FloatRange +import android.annotation.SuppressLint import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -34,6 +36,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.pairwise +import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,6 +79,8 @@ constructor( * single state. This prevent the redundant filters from running. */ private val transitionValueCache = mutableMapOf<KeyguardState, MutableSharedFlow<Float>>() + + @SuppressLint("SharedFlowCreation") private fun getTransitionValueFlow(state: KeyguardState): MutableSharedFlow<Float> { return transitionValueCache.getOrPut(state) { MutableSharedFlow<Float>( @@ -90,6 +95,9 @@ constructor( @Deprecated("Not performant - Use something else in this class") val transitions = repository.transitions + val transitionState: StateFlow<TransitionStep> = + transitions.stateIn(scope, SharingStarted.Eagerly, TransitionStep()) + /** * A pair of the most recent STARTED step, and the transition step immediately preceding it. The * transition framework enforces that the previous step is either a CANCELED or FINISHED step, @@ -99,6 +107,7 @@ constructor( * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming * from when we were canceled. */ + @SuppressLint("SharedFlowCreation") val startedStepWithPrecedingStep = repository.transitions .pairwise() @@ -144,9 +153,10 @@ constructor( } /** Given an [edge], return a SharedFlow to collect only relevant [TransitionStep]. */ + @SuppressLint("SharedFlowCreation") fun getOrCreateFlow(edge: Edge): MutableSharedFlow<TransitionStep> { return transitionMap.getOrPut(edge) { - MutableSharedFlow<TransitionStep>( + MutableSharedFlow( extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST ) @@ -180,6 +190,7 @@ constructor( * AOD<->* transition information, mapped to dozeAmount range of AOD (1f) <-> * * (0f). */ + @SuppressLint("SharedFlowCreation") val dozeAmountTransition: Flow<TransitionStep> = repository.transitions .filter { step -> step.from == AOD || step.to == AOD } @@ -201,11 +212,20 @@ constructor( repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } /** The destination state of the last [TransitionState.STARTED] transition. */ + @SuppressLint("SharedFlowCreation") val startedKeyguardState: SharedFlow<KeyguardState> = startedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) + /** The from state of the last [TransitionState.STARTED] transition. */ + // TODO: is it performant to have several SharedFlows side by side instead of one? + @SuppressLint("SharedFlowCreation") + val startedKeyguardFromState: SharedFlow<KeyguardState> = + startedKeyguardTransitionStep + .map { step -> step.from } + .shareIn(scope, SharingStarted.Eagerly, replay = 1) + /** Which keyguard state to use when the device goes to sleep. */ val asleepKeyguardState: StateFlow<KeyguardState> = keyguardRepository.isAodAvailable @@ -243,6 +263,7 @@ constructor( * sufficient. However, if you're having issues with state *during* transitions started after * one or more canceled transitions, you probably need to use [currentKeyguardState]. */ + @SuppressLint("SharedFlowCreation") val finishedKeyguardState: SharedFlow<KeyguardState> = finishedKeyguardTransitionStep .map { step -> step.to } @@ -491,7 +512,19 @@ constructor( return startedKeyguardState.replayCache.last() } + fun getStartedFromState(): KeyguardState { + return startedKeyguardFromState.replayCache.last() + } + fun getFinishedState(): KeyguardState { return finishedKeyguardState.replayCache.last() } + + suspend fun startTransition(info: TransitionInfo) = repository.startTransition(info) + + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) = repository.updateTransition(transitionId, value, state) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 2d944c694310..9443570705c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -103,6 +103,7 @@ constructor( KeyguardState.LOCKSCREEN -> true KeyguardState.GONE -> true KeyguardState.OCCLUDED -> true + KeyguardState.UNDEFINED -> true } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt index d95c38e2697c..3c661861efd2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.CoreStartable +import com.android.systemui.keyguard.domain.interactor.scenetransition.LockscreenSceneTransitionInteractor import dagger.Binds import dagger.Module import dagger.multibindings.ClassKey @@ -31,6 +32,13 @@ abstract class StartKeyguardTransitionModule { abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable @Binds + @IntoMap + @ClassKey(LockscreenSceneTransitionInteractor::class) + abstract fun bindLockscreenSceneTransitionInteractor( + impl: LockscreenSceneTransitionInteractor + ): CoreStartable + + @Binds @IntoSet abstract fun fromPrimaryBouncer( impl: FromPrimaryBouncerTransitionInteractor diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt new file mode 100644 index 000000000000..6e00aa7956e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -0,0 +1,229 @@ +/* + * 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.systemui.keyguard.domain.interactor.scenetransition + +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.SceneKey +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.LockscreenSceneTransitionRepository +import com.android.systemui.keyguard.data.repository.LockscreenSceneTransitionRepository.Companion.DEFAULT_STATE +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.util.kotlin.pairwise +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +/** + * This class listens to scene framework scene transitions and manages keyguard transition framework + * (KTF) states accordingly. + * + * There are a few rules: + * - When scene framework is on a scene outside of Lockscreen, then KTF is in state UNDEFINED + * - When scene framework is on Lockscreen, KTF is allowed to change its scenes freely + * - When scene framework is transitioning away from Lockscreen, then KTF transitions to UNDEFINED + * and shares its progress. + * - When scene framework is transitioning to Lockscreen, then KTF starts a transition to LOCKSCREEN + * but it is allowed to interrupt this transition and transition to other internal KTF states + * + * There are a few notable differences between SceneTransitionLayout (STL) and KTF that require + * special treatment when synchronizing both state machines. + * - STL does not emit cancelations as KTF does + * - Both STL and KTF require state continuity, though the rules from where starting the next + * transition is allowed is different on each side: + * - STL has a concept of "currentScene" which can be chosen to be either A or B in a A -> B + * transition. The currentScene determines which transition can be started next. In KTF the + * currentScene is always the `to` state. Which means transitions can only be started from B. + * This also holds true when A -> B was canceled: the next transition needs to start from B. + * - KTF can not settle back in its from scene, instead it needs to cancel and start a reversed + * transition. + */ +@SysUISingleton +class LockscreenSceneTransitionInteractor +@Inject +constructor( + val transitionInteractor: KeyguardTransitionInteractor, + @Application private val applicationScope: CoroutineScope, + private val sceneInteractor: SceneInteractor, + private val repository: LockscreenSceneTransitionRepository, +) : CoreStartable, SceneInteractor.OnSceneAboutToChangeListener { + + private var currentTransitionId: UUID? = null + private var progressJob: Job? = null + + override fun start() { + sceneInteractor.registerSceneStateProcessor(this) + listenForSceneTransitionProgress() + } + + override fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?) { + if (toScene != Scenes.Lockscreen || sceneState == null) return + if (sceneState !is KeyguardState) { + throw IllegalArgumentException("Lockscreen sceneState needs to be a KeyguardState.") + } + repository.nextLockscreenTargetState.value = sceneState + } + + private fun listenForSceneTransitionProgress() { + applicationScope.launch { + sceneInteractor.transitionState + .pairwise(ObservableTransitionState.Idle(Scenes.Lockscreen)) + .collect { (prevTransition, transition) -> + when (transition) { + is ObservableTransitionState.Idle -> handleIdle(prevTransition, transition) + is ObservableTransitionState.Transition -> handleTransition(transition) + } + } + } + } + + private suspend fun handleIdle( + prevTransition: ObservableTransitionState, + idle: ObservableTransitionState.Idle + ) { + if (currentTransitionId == null) return + if (prevTransition !is ObservableTransitionState.Transition) return + + if (idle.currentScene == prevTransition.toScene) { + finishCurrentTransition() + } else { + val targetState = + if (idle.currentScene == Scenes.Lockscreen) { + transitionInteractor.getStartedFromState() + } else { + UNDEFINED + } + finishReversedTransitionTo(targetState) + } + } + + private fun finishCurrentTransition() { + transitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED) + resetTransitionData() + } + + private suspend fun finishReversedTransitionTo(state: KeyguardState) { + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = transitionInteractor.getStartedState(), + to = state, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.REVERSE + ) + currentTransitionId = transitionInteractor.startTransition(newTransition) + transitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED) + resetTransitionData() + } + + private fun resetTransitionData() { + progressJob?.cancel() + progressJob = null + currentTransitionId = null + } + + private suspend fun handleTransition(transition: ObservableTransitionState.Transition) { + if (transition.fromScene == Scenes.Lockscreen) { + if (currentTransitionId != null) { + val currentToState = transitionInteractor.getStartedState() + if (currentToState == UNDEFINED) { + transitionKtfTo(transitionInteractor.getStartedFromState()) + } + } + startTransitionFromLockscreen() + collectProgress(transition) + } else if (transition.toScene == Scenes.Lockscreen) { + if (currentTransitionId != null) { + transitionKtfTo(UNDEFINED) + } + startTransitionToLockscreen() + collectProgress(transition) + } else { + transitionKtfTo(UNDEFINED) + } + } + + private suspend fun transitionKtfTo(state: KeyguardState) { + val currentTransition = transitionInteractor.transitionState.value + if (currentTransition.isFinishedIn(state)) { + // This is already the state we want to be in + resetTransitionData() + } else if (currentTransition.isTransitioning(to = state)) { + finishCurrentTransition() + } else { + finishReversedTransitionTo(state) + } + } + + private fun collectProgress(transition: ObservableTransitionState.Transition) { + progressJob?.cancel() + progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } } + } + + private suspend fun startTransitionToLockscreen() { + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = UNDEFINED, + to = repository.nextLockscreenTargetState.value, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + repository.nextLockscreenTargetState.value = DEFAULT_STATE + startTransition(newTransition) + } + + private suspend fun startTransitionFromLockscreen() { + val currentState = transitionInteractor.getStartedState() + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = currentState, + to = UNDEFINED, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + startTransition(newTransition) + } + + private suspend fun startTransition(transitionInfo: TransitionInfo) { + if (currentTransitionId != null) { + resetTransitionData() + } + currentTransitionId = transitionInteractor.startTransition(transitionInfo) + } + + private fun updateProgress(progress: Float) { + if (currentTransitionId == null) return + transitionInteractor.updateTransition( + currentTransitionId!!, + progress.coerceIn(0f, 1f), + RUNNING + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index 7d0553937f25..6d96db34e23a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.shared.model /** List of all possible states to transition to/from */ enum class KeyguardState { - /* + /** * The display is completely off, as well as any sensors that would trigger the device to wake * up. */ @@ -29,13 +29,13 @@ enum class KeyguardState { * notifications is enabled, allowing the device to quickly wake up. */ DOZING, - /* + /** * A device state after the device times out, which can be from both LOCKSCREEN or GONE states. * DOZING is an example of special version of this state. Dreams may be implemented by third * parties to present their own UI over keyguard, like a screensaver. */ DREAMING, - /* + /** * A device state after the device times out, which can be from both LOCKSCREEN or GONE states. * It is a special version of DREAMING state but not DOZING. The active dream will be windowless * and hosted in the lockscreen. @@ -47,17 +47,17 @@ enum class KeyguardState { * low-power mode without a UI, then it is DOZING. */ AOD, - /* + /** * The security screen prompt containing UI to prompt the user to use a biometric credential * (ie: fingerprint). When supported, this may show before showing the primary bouncer. */ ALTERNATE_BOUNCER, - /* + /** * The security screen prompt UI, containing PIN, Password, Pattern for the user to verify their * credentials. */ PRIMARY_BOUNCER, - /* + /** * Device is actively displaying keyguard UI and is not in low-power mode. Device may be * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. */ @@ -68,15 +68,20 @@ enum class KeyguardState { * or dream, as well as swipe down for the notifications and up for the bouncer. */ GLANCEABLE_HUB, - /* - * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard - * is being removed, but there are other cases where the user is swiping away keyguard, such as + /** + * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard is + * being removed, but there are other cases where the user is swiping away keyguard, such as * with SWIPE security method or face unlock without bypass. */ GONE, - /* - * An activity is displaying over the keyguard. + /** + * Only used in scene framework. This means we are currently on any scene framework scene that + * is not Lockscreen. Transitions to and from UNDEFINED are always bound to the + * [SceneTransitionLayout] scene transition that either transitions to or from the Lockscreen + * scene. These transitions are automatically handled by [LockscreenSceneTransitionInteractor]. */ + UNDEFINED, + /** An activity is displaying over the keyguard. */ OCCLUDED; companion object { @@ -109,6 +114,7 @@ enum class KeyguardState { LOCKSCREEN -> true GONE -> true OCCLUDED -> true + UNDEFINED -> true } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt index 0fa6f4fa4f0b..2b4c4af98ccd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -30,4 +30,12 @@ constructor( value: Float, transitionState: TransitionState, ) : this(info.from, info.to, value, transitionState, info.ownerName) + + fun isTransitioning(from: KeyguardState? = null, to: KeyguardState? = null): Boolean { + return (from == null || this.from == from) && (to == null || this.to == to) + } + + fun isFinishedIn(state: KeyguardState): Boolean { + return to == state && transitionState == TransitionState.FINISHED + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt index 87324a233cef..6f8389fc8b7c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt @@ -117,7 +117,8 @@ constructor( KeyguardState.DOZING, KeyguardState.DREAMING, KeyguardState.PRIMARY_BOUNCER, - KeyguardState.AOD -> emit(0f) + KeyguardState.AOD, + KeyguardState.UNDEFINED -> emit(0f) KeyguardState.ALTERNATE_BOUNCER, KeyguardState.LOCKSCREEN -> emit(1f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 53b2697a53de..ae83c9e720a3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -148,10 +148,11 @@ constructor( KeyguardState.GLANCEABLE_HUB, KeyguardState.GONE, KeyguardState.OCCLUDED, - KeyguardState.DREAMING_LOCKSCREEN_HOSTED, -> 0f + KeyguardState.DREAMING_LOCKSCREEN_HOSTED, + KeyguardState.UNDEFINED, -> 0f KeyguardState.AOD, KeyguardState.ALTERNATE_BOUNCER, - KeyguardState.LOCKSCREEN -> 1f + KeyguardState.LOCKSCREEN, -> 1f } } val useBackgroundProtection: StateFlow<Boolean> = isUdfpsSupported diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index eabc42b02665..3e2c6306467f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -21,6 +21,7 @@ package com.android.systemui.scene.data.repository import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSource @@ -36,6 +37,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn +@SysUISingleton /** Source of truth for scene framework application state. */ class SceneContainerRepository @Inject diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 08efe39d7674..0d0f6e069d2d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -55,6 +55,18 @@ constructor( private val deviceUnlockedInteractor: DeviceUnlockedInteractor, ) { + interface OnSceneAboutToChangeListener { + + /** + * Notifies that the scene is about to change to [toScene]. + * + * The implementation can choose to consume the [sceneState] to prepare the incoming scene. + */ + fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?) + } + + private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>() + /** * The current scene. * @@ -149,6 +161,10 @@ constructor( return repository.allSceneKeys() } + fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) { + onSceneAboutToChangeListener.add(processor) + } + /** * Requests a scene change to the given scene. * @@ -161,6 +177,7 @@ constructor( toScene: SceneKey, loggingReason: String, transitionKey: TransitionKey? = null, + sceneState: Any? = null, ) { val currentSceneKey = currentScene.value if ( @@ -180,6 +197,7 @@ constructor( isInstant = false, ) + onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(toScene, sceneState) } repository.changeScene(toScene, transitionKey) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 4a6427794def..3ce12dddb08e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -234,7 +234,7 @@ constructor( bouncerInteractor.onImeHiddenByUser.collectLatest { if (sceneInteractor.currentScene.value == Scenes.Bouncer) { sceneInteractor.changeScene( - toScene = Scenes.Lockscreen, + toScene = Scenes.Lockscreen, // TODO(b/336581871): add sceneState? loggingReason = "IME hidden", ) } @@ -252,6 +252,7 @@ constructor( when { isAnySimLocked -> { switchToScene( + // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Bouncer, loggingReason = "Need to authenticate locked SIM card." ) @@ -259,6 +260,7 @@ constructor( unlockStatus.isUnlocked && deviceEntryInteractor.canSwipeToEnter.value == false -> { switchToScene( + // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Gone, loggingReason = "All SIM cards unlocked and device already unlocked and " + @@ -267,6 +269,7 @@ constructor( } else -> { switchToScene( + // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Lockscreen, loggingReason = "All SIM cards unlocked and device still locked" + @@ -325,7 +328,8 @@ constructor( Scenes.Gone to "device was unlocked in Bouncer scene" } else { val prevScene = previousScene.value - (prevScene ?: Scenes.Gone) to + (prevScene + ?: Scenes.Gone) to "device was unlocked in Bouncer scene, from sceneKey=$prevScene" } isOnLockscreen -> @@ -364,6 +368,7 @@ constructor( powerInteractor.isAsleep.collect { isAsleep -> if (isAsleep) { switchToScene( + // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Lockscreen, loggingReason = "device is starting to sleep", ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt index bd932260848b..969cf482be90 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt @@ -22,6 +22,8 @@ import android.graphics.Insets import android.graphics.Rect import android.graphics.Region import android.util.AttributeSet +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -43,13 +45,43 @@ class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : private val displayMetrics = context.resources.displayMetrics private val tmpRect = Rect() private lateinit var actionsContainerBackground: View + private lateinit var actionsContainer: View private lateinit var dismissButton: View + // Prepare an internal `GestureDetector` to determine when we can initiate a touch-interception + // session (with the client's provided `onTouchInterceptListener`). We delegate out to their + // listener only for gestures that can't be handled by scrolling our `actionsContainer`. + private val gestureDetector = + GestureDetector( + context, + object : SimpleOnGestureListener() { + override fun onScroll( + ev1: MotionEvent?, + ev2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + actionsContainer.getBoundsOnScreen(tmpRect) + val touchedInActionsContainer = + tmpRect.contains(ev2.rawX.toInt(), ev2.rawY.toInt()) + val canHandleInternallyByScrolling = + touchedInActionsContainer + && actionsContainer.canScrollHorizontally(distanceX.toInt()) + return !canHandleInternallyByScrolling + } + } + ) + init { - setOnTouchListener({ _: View, _: MotionEvent -> + + // Delegate to the client-provided `onTouchInterceptListener` if we've already initiated + // touch-interception. + setOnTouchListener({ _: View, ev: MotionEvent -> userInteractionCallback?.invoke() - true + onTouchInterceptListener?.invoke(ev) ?: false }) + + gestureDetector.setIsLongpressEnabled(false) } override fun onFinishInflate() { @@ -60,7 +92,15 @@ class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : blurredScreenshotPreview = requireViewById(R.id.screenshot_preview_blur) screenshotStatic = requireViewById(R.id.screenshot_static) actionsContainerBackground = requireViewById(R.id.actions_container_background) + actionsContainer = requireViewById(R.id.actions_container) dismissButton = requireViewById(R.id.screenshot_dismiss_button) + + // Configure to extend the timeout during ongoing gestures (i.e. scrolls) that are already + // being handled by our child views. + actionsContainer.setOnTouchListener({ _: View, ev: MotionEvent -> + userInteractionCallback?.invoke() + false + }) } fun getTouchRegion(gestureInsets: Insets): Region { @@ -171,9 +211,16 @@ class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { userInteractionCallback?.invoke() - if (onTouchInterceptListener?.invoke(ev) == true) { - return true + // Let the client-provided listener see all `DOWN` events so that they'll be able to + // interpret the remainder of the gesture, even if interception starts partway-through. + // TODO: is this really necessary? And if we don't go on to start interception, should we + // follow up with `ACTION_CANCEL`? + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + onTouchInterceptListener?.invoke(ev) } - return super.onInterceptTouchEvent(ev) + + // Only allow the client-provided touch interceptor to take over the gesture if our + // top-level `GestureDetector` decides not to scroll the action container. + return gestureDetector.onTouchEvent(ev) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index d2c93da671af..884ccef3a080 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -140,7 +140,7 @@ constructor( private fun animateCollapseShadeInternal() { sceneInteractor.changeScene( - getCollapseDestinationScene(), + getCollapseDestinationScene(), // TODO(b/336581871): add sceneState? "ShadeController.animateCollapseShade", SlightlyFasterShadeCollapse, ) diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt index c9949cdc8ab6..55bd8c6c0834 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt @@ -44,6 +44,7 @@ constructor( } else { Scenes.Shade } + // TODO(b/336581871): add sceneState? sceneInteractor.changeScene(key, "animateCollapseQs") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt index 1a223c110ad5..933eb207e76d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -18,12 +18,10 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import android.os.SystemProperties import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable -import com.android.systemui.Flags.notificationMinimalismPrototype import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager @@ -41,6 +39,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.asIndenting @@ -264,7 +263,7 @@ constructor( } private fun unseenFeatureEnabled(): Flow<Boolean> { - if (notificationMinimalismPrototype()) { + if (NotificationMinimalismPrototype.V1.isEnabled) { return flowOf(true) } return secureSettings @@ -342,18 +341,6 @@ constructor( var hasFilteredAnyNotifs = false /** - * the [notificationMinimalismPrototype] will now show seen notifications on the locked - * shade by default, but this property read allows that to be quickly disabled for - * testing - */ - private val minimalismShowOnLockedShade - get() = - SystemProperties.getBoolean( - "persist.notification_minimalism_prototype.show_on_locked_shade", - true - ) - - /** * Encapsulates a definition of "being on the keyguard". Note that these two definitions * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing] @@ -364,7 +351,10 @@ constructor( * allow seen notifications to appear in the locked shade. */ private fun isOnKeyguard(): Boolean = - if (notificationMinimalismPrototype() && minimalismShowOnLockedShade) { + if ( + NotificationMinimalismPrototype.V1.isEnabled && + NotificationMinimalismPrototype.V1.showOnLockedShade + ) { statusBarStateController.state == StatusBarState.KEYGUARD } else { keyguardRepository.isKeyguardShowing() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt new file mode 100644 index 000000000000..8889a10f5ad1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt @@ -0,0 +1,79 @@ +/* + * 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.systemui.statusbar.notification.shared + +import android.os.SystemProperties +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the minimalism prototype flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationMinimalismPrototype { + object V1 { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the heads-up cycling animation enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationMinimalismPrototype() + + /** + * the prototype will now show seen notifications on the locked shade by default, but this + * property read allows that to be quickly disabled for testing + */ + val showOnLockedShade: Boolean + get() = + if (isUnexpectedlyInLegacyMode()) false + else + SystemProperties.getBoolean( + "persist.notification_minimalism_prototype.show_on_locked_shade", + true + ) + + /** gets the configurable max number of notifications */ + val maxNotifs: Int + get() = + if (isUnexpectedlyInLegacyMode()) -1 + else + SystemProperties.getInt( + "persist.notification_minimalism_prototype.lock_screen_max_notifs", + 1 + ) + + /** + * 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 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/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 5bd4c758d678..a44674542b5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -17,11 +17,9 @@ package com.android.systemui.statusbar.notification.stack import android.content.res.Resources -import android.os.SystemProperties import android.util.Log import android.view.View.GONE import androidx.annotation.VisibleForTesting -import com.android.systemui.Flags.notificationMinimalismPrototype import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -31,6 +29,7 @@ import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Compile import com.android.systemui.util.children @@ -381,16 +380,13 @@ constructor( fun updateResources() { maxKeyguardNotifications = infiniteIfNegative( - if (notificationMinimalismPrototype()) { - SystemProperties.getInt( - "persist.notification_minimalism_prototype.lock_screen_max_notifs", - 1 - ) + if (NotificationMinimalismPrototype.V1.isEnabled) { + NotificationMinimalismPrototype.V1.maxNotifs } else { resources.getInteger(R.integer.keyguard_max_notification_count) } ) - maxNotificationsExcludesMedia = notificationMinimalismPrototype() + maxNotificationsExcludesMedia = NotificationMinimalismPrototype.V1.isEnabled dividerHeight = max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index f0dab3ba1829..b71564627223 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -696,6 +696,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb Trace.beginSection("StatusBarKeyguardViewManager#show"); mNotificationShadeWindowController.setKeyguardShowing(true); if (SceneContainerFlag.isEnabled()) { + // TODO(b/336581871): add sceneState? mSceneInteractorLazy.get().changeScene( Scenes.Lockscreen, "StatusBarKeyguardViewManager.show"); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 8e4c155593e2..fd37cad72371 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -8,6 +8,7 @@ import android.content.pm.ApplicationInfo import android.graphics.Point import android.graphics.Rect import android.os.Looper +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.IRemoteAnimationFinishedCallback @@ -17,15 +18,20 @@ import android.view.SurfaceControl import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout +import android.window.RemoteTransition +import android.window.TransitionFilter import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.Flags import com.android.systemui.util.mockito.any +import com.android.wm.shell.shared.ShellTransitions import junit.framework.Assert.assertFalse import junit.framework.Assert.assertNotNull import junit.framework.Assert.assertNull import junit.framework.Assert.assertTrue import junit.framework.AssertionFailedError import kotlin.concurrent.thread +import kotlin.test.assertEquals import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before @@ -48,6 +54,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { private val transitionContainer = LinearLayout(mContext) private val mainExecutor = context.mainExecutor private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor) + private val testShellTransitions = FakeShellTransitions() @Mock lateinit var callback: ActivityTransitionAnimator.Callback @Mock lateinit var listener: ActivityTransitionAnimator.Listener @Spy private val controller = TestTransitionAnimatorController(transitionContainer) @@ -55,12 +62,16 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { private lateinit var activityTransitionAnimator: ActivityTransitionAnimator @get:Rule val rule = MockitoJUnit.rule() + @get:Rule val setFlagsRule = SetFlagsRule() @Before fun setup() { activityTransitionAnimator = ActivityTransitionAnimator( mainExecutor, + ActivityTransitionAnimator.TransitionRegister.fromShellTransitions( + testShellTransitions + ), testTransitionAnimator, testTransitionAnimator, disableWmTimeout = true, @@ -164,6 +175,34 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @Test + fun registersReturnIffCookieIsPresent() { + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + `when`(callback.isOnKeyguard()).thenReturn(false) + + startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> + ActivityManager.START_DELIVERED_TO_TOP + } + + waitForIdleSync() + assertTrue(testShellTransitions.remotes.isEmpty()) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + + val controller = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie + get() = ActivityTransitionAnimator.TransitionCookie("testCookie") + } + + startIntentWithAnimation(activityTransitionAnimator, controller) { _ -> + ActivityManager.START_DELIVERED_TO_TOP + } + + waitForIdleSync() + assertEquals(1, testShellTransitions.remotes.size) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + } + + @Test fun doesNotStartIfAnimationIsCancelled() { val runner = activityTransitionAnimator.createRunner(controller) runner.onAnimationCancelled() @@ -243,6 +282,35 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } /** + * A fake implementation of [ShellTransitions] which saves filter-transition pairs locally and + * allows inspection. + */ +private class FakeShellTransitions : ShellTransitions { + val remotes = mutableMapOf<TransitionFilter, RemoteTransition>() + val remotesForTakeover = mutableMapOf<TransitionFilter, RemoteTransition>() + + override fun registerRemote(filter: TransitionFilter, remoteTransition: RemoteTransition) { + remotes[filter] = remoteTransition + } + + override fun registerRemoteForTakeover( + filter: TransitionFilter, + remoteTransition: RemoteTransition + ) { + remotesForTakeover[filter] = remoteTransition + } + + override fun unregisterRemote(remoteTransition: RemoteTransition) { + while (remotes.containsValue(remoteTransition)) { + remotes.values.remove(remoteTransition) + } + while (remotesForTakeover.containsValue(remoteTransition)) { + remotesForTakeover.values.remove(remoteTransition) + } + } +} + +/** * A simple implementation of [ActivityTransitionAnimator.Controller] which throws if it is called * outside of the main thread. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt index b31fe21f8e91..42fcd547408a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt @@ -16,12 +16,16 @@ package com.android.systemui.animation +import android.os.HandlerThread import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.view.View import android.widget.FrameLayout import androidx.test.filters.SmallTest +import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.animation.view.LaunchableFrameLayout +import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -30,6 +34,13 @@ import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { + companion object { + private const val LAUNCH_CUJ = 0 + private const val RETURN_CUJ = 1 + } + + private val interactionJankMonitor = FakeInteractionJankMonitor() + @Test fun animatingOrphanViewDoesNotCrash() { val state = TransitionAnimator.State(top = 0, bottom = 0, left = 0, right = 0) @@ -47,4 +58,63 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { GhostedViewTransitionAnimatorController(FrameLayout(mContext)) } } + + @Test + fun cujsAreLoggedCorrectly() { + val parent = FrameLayout(mContext) + + val launchView = LaunchableFrameLayout(mContext) + parent.addView((launchView)) + val launchController = + GhostedViewTransitionAnimatorController( + launchView, + launchCujType = LAUNCH_CUJ, + returnCujType = RETURN_CUJ, + interactionJankMonitor = interactionJankMonitor + ) + launchController.onTransitionAnimationStart(isExpandingFullyAbove = true) + assertThat(interactionJankMonitor.ongoing).containsExactly(LAUNCH_CUJ) + launchController.onTransitionAnimationEnd(isExpandingFullyAbove = true) + assertThat(interactionJankMonitor.ongoing).isEmpty() + assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ) + + val returnView = LaunchableFrameLayout(mContext) + parent.addView((returnView)) + val returnController = + object : GhostedViewTransitionAnimatorController( + returnView, + launchCujType = LAUNCH_CUJ, + returnCujType = RETURN_CUJ, + interactionJankMonitor = interactionJankMonitor + ) { + override val isLaunching = false + } + returnController.onTransitionAnimationStart(isExpandingFullyAbove = true) + assertThat(interactionJankMonitor.ongoing).containsExactly(RETURN_CUJ) + returnController.onTransitionAnimationEnd(isExpandingFullyAbove = true) + assertThat(interactionJankMonitor.ongoing).isEmpty() + assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ) + } + + /** + * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and + * allows inspection. + */ + private class FakeInteractionJankMonitor : InteractionJankMonitor( + HandlerThread("testThread") + ) { + val ongoing: MutableSet<Int> = mutableSetOf() + val finished: MutableSet<Int> = mutableSetOf() + + override fun begin(v: View?, cujType: Int): Boolean { + ongoing.add(cujType) + return true + } + + override fun end(cujType: Int): Boolean { + ongoing.remove(cujType) + finished.add(cujType) + return true + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt new file mode 100644 index 000000000000..d0d9891a953f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt @@ -0,0 +1,1320 @@ +/* + * 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.systemui.keyguard.domain.interactor.scenetransition + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.data.repository.sceneContainerRepository +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Ignore +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { + private val kosmos = + testKosmos().apply { keyguardTransitionRepository = realKeyguardTransitionRepository } + + private val testScope = kosmos.testScope + private val underTest = kosmos.lockscreenSceneTransitionInteractor + + private val progress = MutableStateFlow(0f) + + private val sceneTransitions = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) + + private val lsToGone = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Gone, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + private val goneToLs = + ObservableTransitionState.Transition( + Scenes.Gone, + Scenes.Lockscreen, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + @Before + fun setUp() { + underTest.start() + kosmos.sceneContainerRepository.setTransitionState(sceneTransitions) + testScope.launch { + kosmos.realKeyguardTransitionRepository.emitInitialStepsFromOff( + KeyguardState.LOCKSCREEN + ) + } + } + + /** STL: Ls -> Gone, then settle with Idle(Gone). This is the default case. */ + @Test + fun transition_from_ls_scene_end_in_gone() = + testScope.runTest { + sceneTransitions.value = lsToGone + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Ls -> Gone, then settle with Idle(Ls). KTF in this scenario needs to invert the + * transition LS -> UNDEFINED to UNDEFINED -> LS as there is no mechanism in KTF to + * finish/settle to progress 0.0f. + */ + @Test + fun transition_from_ls_scene_end_in_ls() = + testScope.runTest { + sceneTransitions.value = lsToGone + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.STARTED, + progress = 0.6f, + ) + + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Ls -> Gone, then settle with Idle(Ls). KTF starts in AOD and needs to inverse correctly + * back to AOD. + */ + @Test + fun transition_from_ls_scene_on_aod_end_in_ls() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + sceneTransitions.value = lsToGone + + assertTransition( + step = currentStep!!, + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.UNDEFINED, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0.6f, + ) + + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.UNDEFINED, + to = KeyguardState.AOD, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Gone -> Ls, then settle with Idle(Ls). This is the default case in the reverse + * direction. + */ + @Test + fun transition_to_ls_scene_end_in_ls() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = goneToLs + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** STL: Gone -> Ls (AOD), will transition to AOD once */ + @Test + fun transition_to_ls_scene_with_changed_next_scene_is_respected_just_once() = + testScope.runTest { + underTest.onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD) + sceneTransitions.value = goneToLs + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.AOD, + state = TransitionState.RUNNING, + progress = 0f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Shade, + Scenes.Lockscreen, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + } + + /** + * STL: Gone -> Ls, then settle with Idle(Gone). KTF in this scenario needs to invert the + * transition UNDEFINED -> LS to LS -> UNDEFINED as there is no mechanism in KTF to + * finish/settle to progress 0.0f. + */ + @Test + fun transition_to_ls_scene_end_in_from_scene() = + testScope.runTest { + sceneTransitions.value = goneToLs + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone) + val stepM3 = allSteps[allSteps.size - 3] + val stepM2 = allSteps[allSteps.size - 2] + + assertTransition( + step = stepM3, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = stepM2, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0.6f, + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Gone -> Ls, then interrupted by Shade -> Ls. KTF in this scenario needs to invert the + * transition UNDEFINED -> LS to LS -> UNDEFINED as there is no mechanism in KTF to + * finish/settle to progress 0.0f. Then restart a different transition UNDEFINED -> Ls. + */ + @Test + fun transition_to_ls_scene_end_in_to_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = goneToLs + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Shade, + Scenes.Lockscreen, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 5], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = allSteps[allSteps.size - 4], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0.6f, + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.2f + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.2f, + ) + } + + /** + * STL: Gone -> Ls, then interrupted by Ls -> Shade. This is like continuing the transition from + * Ls before the transition before has properly settled. This can happen in STL e.g. with an + * accelerated swipe (quick successive fling gestures). + */ + @Test + fun transition_to_ls_scene_end_in_from_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = goneToLs + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Shade, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0.0f, + ) + + progress.value = 0.2f + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.2f, + ) + } + + /** + * STL: Gone -> Ls, then interrupted by Gone -> Shade. This is going back to Gone but starting a + * transition from Gone before settling in Gone. KTF needs to make sure the transition is + * properly inversed and settled in UNDEFINED. + */ + @Test + fun transition_to_ls_scene_end_in_other_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = goneToLs + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Gone, + Scenes.Shade, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0.6f, + ) + + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then stl still finishes in Ls. After a KTF + * transition is started (UNDEFINED -> LOCKSCREEN) KTF immediately considers the active scene to + * be LOCKSCREEN. This means that all listeners for LOCKSCREEN are active and may start a new + * transition LOCKSCREEN -> *. Here we test LS -> AOD. + * + * KTF is allowed to already start and play the other transition, while the STL transition may + * finish later (gesture completes much later). When we eventually settle the STL transition in + * Ls we do not want to force KTF back to its original destination (LOCKSCREEN). Instead, for + * this scenario the settle can be ignored. + */ + @Test + fun transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_lockscreen() = + testScope.runTest { + sceneTransitions.value = goneToLs + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + // Scene progress should not affect KTF transition anymore + progress.value = 0.7f + assertTransition(currentStep!!, progress = 0f) + + // Scene transition still finishes but should not impact KTF transition + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + } + + /** + * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then stl finishes in Gone. + * + * Refers to: `transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_lockscreen` + * + * This is similar to the previous scenario but the gesture may have gone back to its origin. In + * this case we can not ignore the settlement, because whatever KTF has done in the meantime it + * needs to immediately finish in UNDEFINED (there is a jump cut). + */ + @Test + fun transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_gone() = + testScope.runTest { + sceneTransitions.value = goneToLs + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.7f + assertThat(currentStep?.value).isEqualTo(0f) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone) + + assertTransition( + step = currentStep!!, + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then STL Gone -> Shade + * + * Refers to: `transition_to_ls_scene_interrupted_by_ktf_transition_then_finish_in_lockscreen` + * + * This is similar to the previous scenario but the gesture may have been interrupted by any + * other transition. KTF needs to immediately finish in UNDEFINED (there is a jump cut). + */ + @Test + fun transition_to_ls_interrupted_by_ktf_transition_then_interrupted_by_other_transition() = + testScope.runTest { + sceneTransitions.value = goneToLs + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition( + step = currentStep!!, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.7f + assertTransition(currentStep!!, progress = 0f) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Gone, + Scenes.Shade, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + /** + * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then STL Ls -> Shade + * + * In this scenario it is important that the last STL transition Ls -> Shade triggers a cancel + * of the * -> AOD transition but then also properly starts a transition AOD (not LOCKSCREEN) -> + * UNDEFINED transition. + */ + @Test + fun transition_to_ls_interrupted_by_ktf_transition_then_interrupted_by_from_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = goneToLs + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.7f + assertTransition(currentStep!!, progress = 0f) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Shade, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + allSteps[allSteps.size - 3] + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.CANCELED, + progress = 0f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.2f + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.2f, + ) + } + + /** + * STL: Gone -> Ls, then interrupt in KTF LS -> AOD, then STL Shade -> Ls + * + * In this scenario it is important KTF is brought back into a FINISHED UNDEFINED state + * considering the state is already on AOD from where a new UNDEFINED -> LOCKSCREEN transition + * can be started. + */ + @Test + fun transition_to_ls_interrupted_by_ktf_transition_then_interrupted_by_to_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = goneToLs + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.7f + assertTransition(currentStep!!, progress = 0f) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Shade, + Scenes.Lockscreen, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 5], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.CANCELED, + progress = 0f, + ) + + assertTransition( + step = allSteps[allSteps.size - 4], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 1f, + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.STARTED, + progress = 0f, + ) + + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.7f, + ) + } + + /** + * STL: Gone -> Ls, then interrupt multiple canceled KTF transitions, then STL Ls -> Shade + * + * Similar to + * `transition_to_ls_scene_interrupted_by_ktf_transition_then_interrupted_by_from_ls_transition` + * but here KTF is canceled multiple times such that in the end OCCLUDED -> UNDEFINED is + * properly started. (not from AOD or LOCKSCREEN) + * + * Note: there is no test which tests multiple cancels from the STL side, this is because all + * STL transitions trigger a response from LockscreenSceneTransitionInteractor which forces KTF + * into a specific state, so testing each pair is enough. Meanwhile KTF can move around without + * any reaction from LockscreenSceneTransitionInteractor. + */ + @Test + fun transition_to_ls_interrupted_by_ktf_cancel_sequence_interrupted_by_from_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = lsToGone + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.AOD, + to = KeyguardState.DOZING, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.AOD, + to = KeyguardState.DOZING, + state = TransitionState.STARTED, + progress = 0f, + ) + + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.DOZING, + to = KeyguardState.OCCLUDED, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.DOZING, + to = KeyguardState.OCCLUDED, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.7f + assertTransition(currentStep!!, progress = 0f) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Shade, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.DOZING, + to = KeyguardState.OCCLUDED, + state = TransitionState.CANCELED, + progress = 0f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.OCCLUDED, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.2f + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.OCCLUDED, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.2f, + ) + } + + /** + * STL: Gone -> Ls, then interrupted by KTF LS -> AOD which is FINISHED before STL Ls -> Shade + * + * Similar to + * `transition_to_ls_scene_interrupted_by_ktf_transition_then_interrupted_by_from_ls_transition` + * but here KTF is finishing the transition and only then gets interrupted. Should correctly + * start AOD -> UNDEFINED. + */ + @Test + fun transition_to_ls_scene_interrupted_and_finished_by_ktf_interrupted_by_from_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = lsToGone + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + val ktfUuid = + kosmos.realKeyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = this.javaClass.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.STARTED, + progress = 0f, + ) + + kosmos.realKeyguardTransitionRepository.updateTransition( + ktfUuid!!, + 1f, + TransitionState.FINISHED + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.FINISHED, + progress = 1f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Shade, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + state = TransitionState.FINISHED, + progress = 1f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.2f + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.AOD, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.2f, + ) + } + + /** + * STL: Ls -> Gone, then interrupted by Ls -> Bouncer. This happens when the next transition is + * immediately started from Gone without settling in Idle. This specifically happens when + * dragging down on Ls and then changing direction. The transition will switch from -> Shade to + * -> Bouncer without settling or signaling any cancellation as STL considers this to be the + * same gesture. + * + * In STL there is no guarantee that transitions settle in Idle before continuing. + */ + @Ignore("Suffers from a race condition that will be fixed in followup CL") + @Test + fun transition_from_ls_scene_interrupted_by_other_from_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = lsToGone + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Bouncer, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 5], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.CANCELED, + progress = 0.4f, + ) + + assertTransition( + step = allSteps[allSteps.size - 4], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.STARTED, + progress = 0.6f, + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.FINISHED, + progress = 1f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.STARTED, + progress = 0f, + ) + } + + /** + * STL: Ls -> Gone, then interrupted by Gone -> Ls. This happens when the next transition is + * immediately started from Gone without settling in Idle. In STL there is no guarantee that + * transitions settle in Idle before continuing. + */ + @Test + fun transition_from_ls_scene_interrupted_by_to_ls_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + val allSteps by collectValues(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = lsToGone + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Gone, + Scenes.Lockscreen, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = allSteps[allSteps.size - 3], + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + + assertTransition( + step = allSteps[allSteps.size - 2], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.STARTED, + progress = 0f, + ) + + progress.value = 0.2f + assertTransition( + step = allSteps[allSteps.size - 1], + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0.2f, + ) + } + + /** + * STL: Ls -> Gone, then interrupted by Gone -> Bouncer. This happens when the next transition + * is immediately started from Gone without settling in Idle. In STL there is no guarantee that + * transitions settle in Idle before continuing. + */ + @Test + fun transition_from_ls_scene_interrupted_by_other_stl_transition() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = lsToGone + progress.value = 0.4f + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Gone, + Scenes.Bouncer, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false) + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + + private fun assertTransition( + step: TransitionStep, + from: KeyguardState? = null, + to: KeyguardState? = null, + state: TransitionState? = null, + progress: Float? = null + ) { + if (from != null) assertThat(step.from).isEqualTo(from) + if (to != null) assertThat(step.to).isEqualTo(to) + if (state != null) assertThat(step.transitionState).isEqualTo(state) + if (progress != null) assertThat(step.value).isEqualTo(progress) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 04fa5904d2e7..845744a54791 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -42,13 +42,10 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.keyguard.data.repository.FakeCommandQueue; import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor; -import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.kosmos.KosmosJavaAdapter; @@ -59,9 +56,7 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.qs.QSFragmentLegacy; import com.android.systemui.res.R; -import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; -import com.android.systemui.scene.shared.logger.SceneLogger; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.domain.interactor.ShadeInteractor; @@ -176,12 +171,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { protected Handler mMainHandler; protected LockscreenShadeTransitionController.Callback mLockscreenShadeTransitionCallback; - protected final ShadeExpansionStateManager mShadeExpansionStateManager = - new ShadeExpansionStateManager(); - protected FragmentHostManager.FragmentListener mFragmentListener; - private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor; - private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor; @Before public void setup() { @@ -190,19 +180,11 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mStatusBarStateController = mKosmos.getStatusBarStateController(); mKosmos.getFakeDeviceProvisioningRepository().setDeviceProvisioned(true); - FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); PowerInteractor powerInteractor = mKosmos.getPowerInteractor(); - SceneInteractor sceneInteractor = new SceneInteractor( - mTestScope.getBackgroundScope(), - new SceneContainerRepository( - mTestScope.getBackgroundScope(), - mKosmos.getFakeSceneContainerConfig(), - mKosmos.getSceneDataSource()), - mock(SceneLogger.class), - mKosmos.getDeviceUnlockedInteractor()); + SceneInteractor sceneInteractor = mKosmos.getSceneInteractor(); KeyguardTransitionInteractor keyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor(); @@ -220,10 +202,6 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { () -> mKosmos.getSharedNotificationContainerInteractor(), mTestScope); - mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor(); - mFromPrimaryBouncerTransitionInteractor = - mKosmos.getFromPrimaryBouncerTransitionInteractor(); - ResourcesSplitShadeStateController splitShadeStateController = new ResourcesSplitShadeStateController(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt index 408157b7614f..3e69e875cd0d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt @@ -17,7 +17,10 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher var Kosmos.keyguardTransitionRepository: KeyguardTransitionRepository by Kosmos.Fixture { fakeKeyguardTransitionRepository } var Kosmos.fakeKeyguardTransitionRepository by Kosmos.Fixture { FakeKeyguardTransitionRepository() } +var Kosmos.realKeyguardTransitionRepository: KeyguardTransitionRepository by + Kosmos.Fixture { KeyguardTransitionRepositoryImpl(testDispatcher) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt new file mode 100644 index 000000000000..7d0e8b1b4b53 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt @@ -0,0 +1,22 @@ +/* + * 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.systemui.keyguard.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.lockscreenSceneTransitionRepository by + Kosmos.Fixture { LockscreenSceneTransitionRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt new file mode 100644 index 000000000000..3c1f7b1b2394 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -0,0 +1,33 @@ +/* + * 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.systemui.keyguard.domain.interactor.scenetransition + +import com.android.systemui.keyguard.data.repository.lockscreenSceneTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.scene.domain.interactor.sceneInteractor + +var Kosmos.lockscreenSceneTransitionInteractor by + Kosmos.Fixture { + LockscreenSceneTransitionInteractor( + transitionInteractor = keyguardTransitionInteractor, + applicationScope = applicationCoroutineScope, + sceneInteractor = sceneInteractor, + repository = lockscreenSceneTransitionRepository, + ) + } diff --git a/services/backup/java/com/android/server/backup/transport/TransportConnection.java b/services/backup/java/com/android/server/backup/transport/TransportConnection.java index 1009787bebe5..67ebb3e6a1ef 100644 --- a/services/backup/java/com/android/server/backup/transport/TransportConnection.java +++ b/services/backup/java/com/android/server/backup/transport/TransportConnection.java @@ -658,11 +658,13 @@ public class TransportConnection { * This class is a proxy to TransportClient methods that doesn't hold a strong reference to the * TransportClient, allowing it to be GC'ed. If the reference was lost it logs a message. */ - private static class TransportConnectionMonitor implements ServiceConnection { + @VisibleForTesting + static class TransportConnectionMonitor implements ServiceConnection { private final Context mContext; private final WeakReference<TransportConnection> mTransportClientRef; - private TransportConnectionMonitor(Context context, + @VisibleForTesting + TransportConnectionMonitor(Context context, TransportConnection transportConnection) { mContext = context; mTransportClientRef = new WeakReference<>(transportConnection); @@ -704,7 +706,13 @@ public class TransportConnection { /** @see TransportConnection#finalize() */ private void referenceLost(String caller) { - mContext.unbindService(this); + try { + mContext.unbindService(this); + } catch (IllegalArgumentException e) { + TransportUtils.log(Priority.WARN, TAG, + caller + " called but unbindService failed: " + e.getMessage()); + return; + } TransportUtils.log( Priority.INFO, TAG, diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 1db3483e3800..ad93f6fc8d9f 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -55,6 +55,7 @@ import static android.app.AppOpsManager.SAMPLING_STRATEGY_RARELY_USED; import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM; import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM_OPS; import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE; +import static android.app.AppOpsManager.UID_STATE_NONEXISTENT; import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES; import static android.app.AppOpsManager._NUM_OP; import static android.app.AppOpsManager.extractFlagsFromKey; @@ -70,7 +71,6 @@ import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; -import static android.permission.flags.Flags.runtimePermissionAppopsMappingEnabled; import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS; @@ -130,6 +130,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.storage.StorageManagerInternal; import android.permission.PermissionManager; +import android.permission.flags.Flags; import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; @@ -140,6 +141,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import android.util.SparseLongArray; import android.util.TimeUtils; import android.util.Xml; @@ -153,7 +155,6 @@ import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; import com.android.internal.app.IAppOpsStartedCallback; import com.android.internal.app.MessageSamplingConfig; -import com.android.internal.camera.flags.Flags; import com.android.internal.compat.IPlatformCompat; import com.android.internal.os.Clock; import com.android.internal.pm.pkg.component.ParsedAttribution; @@ -1421,6 +1422,9 @@ public class AppOpsService extends IAppOpsService.Stub { // The callback method from AppOpsUidStateTracker private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) { synchronized (this) { + if (state == UID_STATE_NONEXISTENT) { + onUidProcessDeathLocked(uid); + } UidState uidState = getUidStateLocked(uid, false); boolean hasForegroundWatchers = false; @@ -1508,6 +1512,11 @@ public class AppOpsService extends IAppOpsService.Stub { } } + if (state == UID_STATE_NONEXISTENT) { + // For UID_STATE_NONEXISTENT, we don't call onUidStateChanged for AttributedOps + return; + } + if (uidState != null) { int numPkgs = uidState.pkgOps.size(); for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { @@ -1532,6 +1541,81 @@ public class AppOpsService extends IAppOpsService.Stub { } } + @GuardedBy("this") + private void onUidProcessDeathLocked(int uid) { + if (!mUidStates.contains(uid) || !Flags.finishRunningOpsForKilledPackages()) { + return; + } + final SparseLongArray chainsToFinish = new SparseLongArray(); + doForAllAttributedOpsInUidLocked(uid, (attributedOp) -> { + attributedOp.doForAllInProgressStartOpEvents((event) -> { + int chainId = event.getAttributionChainId(); + if (chainId != ATTRIBUTION_CHAIN_ID_NONE) { + long currentEarliestStartTime = + chainsToFinish.get(chainId, Long.MAX_VALUE); + if (event.getStartTime() < currentEarliestStartTime) { + // Store the earliest chain link we're finishing, so that we can go back + // and finish any links in the chain that started after this one + chainsToFinish.put(chainId, event.getStartTime()); + } + } + attributedOp.finished(event.getClientId()); + }); + }); + finishChainsLocked(chainsToFinish); + } + + @GuardedBy("this") + private void finishChainsLocked(SparseLongArray chainsToFinish) { + doForAllAttributedOpsLocked((attributedOp) -> { + attributedOp.doForAllInProgressStartOpEvents((event) -> { + int chainId = event.getAttributionChainId(); + // If this event is part of a chain, and this event started after the event in the + // chain we already finished, then finish this event, too + long earliestEventStart = chainsToFinish.get(chainId, Long.MAX_VALUE); + if (chainId != ATTRIBUTION_CHAIN_ID_NONE + && event.getStartTime() >= earliestEventStart) { + attributedOp.finished(event.getClientId()); + } + }); + }); + } + + @GuardedBy("this") + private void doForAllAttributedOpsLocked(Consumer<AttributedOp> action) { + int numUids = mUidStates.size(); + for (int uidNum = 0; uidNum < numUids; uidNum++) { + int uid = mUidStates.keyAt(uidNum); + doForAllAttributedOpsInUidLocked(uid, action); + } + } + + @GuardedBy("this") + private void doForAllAttributedOpsInUidLocked(int uid, Consumer<AttributedOp> action) { + UidState uidState = mUidStates.get(uid); + if (uidState == null) { + return; + } + + int numPkgs = uidState.pkgOps.size(); + for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { + Ops ops = uidState.pkgOps.valueAt(pkgNum); + int numOps = ops.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + Op op = ops.valueAt(opNum); + int numDevices = op.mDeviceAttributedOps.size(); + for (int deviceNum = 0; deviceNum < numDevices; deviceNum++) { + ArrayMap<String, AttributedOp> attrOps = + op.mDeviceAttributedOps.valueAt(deviceNum); + int numAttributions = attrOps.size(); + for (int attrNum = 0; attrNum < numAttributions; attrNum++) { + action.accept(attrOps.valueAt(attrNum)); + } + } + } + } + } + /** * Notify the proc state or capability has changed for a certain UID. */ @@ -2702,7 +2786,7 @@ public class AppOpsService extends IAppOpsService.Stub { * have information on them. */ private static boolean isOpAllowedForUid(int uid) { - return runtimePermissionAppopsMappingEnabled() + return Flags.runtimePermissionAppopsMappingEnabled() && (uid == Process.ROOT_UID || uid == Process.SYSTEM_UID); } @@ -4775,8 +4859,8 @@ public class AppOpsService extends IAppOpsService.Stub { if ((code == OP_CAMERA) && isAutomotive()) { final long identity = Binder.clearCallingIdentity(); try { - if ((Flags.cameraPrivacyAllowlist()) - && (mSensorPrivacyManager.isCameraPrivacyEnabled(packageName))) { + if (com.android.internal.camera.flags.Flags.cameraPrivacyAllowlist() + && mSensorPrivacyManager.isCameraPrivacyEnabled(packageName)) { return true; } } finally { diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java b/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java index 18ea8cfc1386..268b286d8fe1 100644 --- a/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java +++ b/services/core/java/com/android/server/appop/AppOpsUidStateTracker.java @@ -68,6 +68,7 @@ interface AppOpsUidStateTracker { return UID_STATE_BACKGROUND; } + // UID_STATE_NONEXISTENT is deliberately excluded here return UID_STATE_CACHED; } diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java index bc6ef2005584..03c81560be89 100644 --- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java @@ -34,7 +34,9 @@ import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_TAKE_AUDIO_FOCUS; import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE; import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED; +import static android.app.AppOpsManager.UID_STATE_NONEXISTENT; import static android.app.AppOpsManager.UID_STATE_TOP; +import static android.permission.flags.Flags.finishRunningOpsForKilledPackages; import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState; @@ -343,13 +345,14 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { int capability = mCapability.get(uid, PROCESS_CAPABILITY_NONE); boolean appWidgetVisible = mAppWidgetVisible.get(uid, false); + boolean foregroundChange = uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED + != pendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED + || capability != pendingCapability + || appWidgetVisible != pendingAppWidgetVisible; + if (uidState != pendingUidState || capability != pendingCapability || appWidgetVisible != pendingAppWidgetVisible) { - boolean foregroundChange = uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED - != pendingUidState <= UID_STATE_MAX_LAST_NON_RESTRICTED - || capability != pendingCapability - || appWidgetVisible != pendingAppWidgetVisible; if (foregroundChange) { // To save on memory usage, log only interesting changes. @@ -372,6 +375,16 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { mCapability.delete(uid); mAppWidgetVisible.delete(uid); mPendingGone.delete(uid); + if (finishRunningOpsForKilledPackages()) { + for (int i = 0; i < mUidStateChangedCallbacks.size(); i++) { + UidStateChangedCallback cb = mUidStateChangedCallbacks.keyAt(i); + Executor executor = mUidStateChangedCallbacks.valueAt(i); + + executor.execute(PooledLambda.obtainRunnable( + UidStateChangedCallback::onUidStateChanged, cb, uid, + UID_STATE_NONEXISTENT, foregroundChange)); + } + } } else { mUidStates.put(uid, pendingUidState); mCapability.put(uid, pendingCapability); diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 2760ccf72f98..02fc9938c02c 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -38,6 +38,7 @@ import com.android.internal.util.function.pooled.PooledLambda; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; +import java.util.function.Consumer; final class AttributedOp { private final @NonNull AppOpsService mAppOpsService; @@ -256,6 +257,19 @@ final class AttributedOp { } } + public void doForAllInProgressStartOpEvents(Consumer<InProgressStartOpEvent> action) { + ArrayMap<IBinder, AttributedOp.InProgressStartOpEvent> events = isPaused() + ? mPausedInProgressEvents : mInProgressEvents; + if (events == null) { + return; + } + + int numStartedOps = events.size(); + for (int i = 0; i < numStartedOps; i++) { + action.accept(events.valueAt(i)); + } + } + /** * Update state when finishOp was called. Will finish started ops, and delete paused ops. * diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index e2c4b4638207..cae169550d9a 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -347,9 +347,6 @@ public class SpatializerHelper { //------------------------------------------------------ // routing monitoring synchronized void onRoutingUpdated() { - if (!mFeatureEnabled) { - return; - } switch (mState) { case STATE_UNINITIALIZED: case STATE_NOT_SUPPORTED: @@ -393,7 +390,7 @@ public class SpatializerHelper { setDispatchAvailableState(false); } - boolean enabled = able && enabledAvailable.first; + boolean enabled = mFeatureEnabled && able && enabledAvailable.first; if (enabled) { loglogi("Enabling Spatial Audio since enabled for media device:" + currentDevice); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 61054a9d4de5..4f87c83bb0d7 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -593,6 +593,8 @@ public class NotificationManagerService extends SystemService { static final long NOTIFICATION_TTL = Duration.ofDays(3).toMillis(); + static final long NOTIFICATION_MAX_AGE_AT_POST = Duration.ofDays(14).toMillis(); + private IActivityManager mAm; private ActivityTaskManagerInternal mAtm; private ActivityManager mActivityManager; @@ -2637,27 +2639,48 @@ public class NotificationManagerService extends SystemService { * Cleanup broadcast receivers change listeners. */ public void onDestroy() { - getContext().unregisterReceiver(mIntentReceiver); - getContext().unregisterReceiver(mPackageIntentReceiver); + if (mIntentReceiver != null) { + getContext().unregisterReceiver(mIntentReceiver); + } + if (mPackageIntentReceiver != null) { + getContext().unregisterReceiver(mPackageIntentReceiver); + } if (Flags.allNotifsNeedTtl()) { - mTtlHelper.destroy(); + if (mTtlHelper != null) { + mTtlHelper.destroy(); + } } else { - getContext().unregisterReceiver(mNotificationTimeoutReceiver); + if (mNotificationTimeoutReceiver != null) { + getContext().unregisterReceiver(mNotificationTimeoutReceiver); + } + } + if (mRestoreReceiver != null) { + getContext().unregisterReceiver(mRestoreReceiver); + } + if (mLocaleChangeReceiver != null) { + getContext().unregisterReceiver(mLocaleChangeReceiver); + } + if (mSettingsObserver != null) { + mSettingsObserver.destroy(); + } + if (mRoleObserver != null) { + mRoleObserver.destroy(); } - getContext().unregisterReceiver(mRestoreReceiver); - getContext().unregisterReceiver(mLocaleChangeReceiver); - - mSettingsObserver.destroy(); - mRoleObserver.destroy(); if (mShortcutHelper != null) { mShortcutHelper.destroy(); } - mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_PREFERENCES); - mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES); - mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES); - mStatsManager.clearPullAtomCallback(DND_MODE_RULE); - mAppOps.stopWatchingMode(mAppOpsListener); - mAlarmManager.cancelAll(); + if (mStatsManager != null) { + mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_PREFERENCES); + mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES); + mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES); + mStatsManager.clearPullAtomCallback(DND_MODE_RULE); + } + if (mAppOps != null) { + mAppOps.stopWatchingMode(mAppOpsListener); + } + if (mAlarmManager != null) { + mAlarmManager.cancelAll(); + } } protected String[] getStringArrayResource(int key) { @@ -7722,6 +7745,9 @@ public class NotificationManagerService extends SystemService { return true; } // Check if an app has been given system exemption + if (ai.uid == Process.SYSTEM_UID) { + return false; + } return mAppOps.checkOpNoThrow( AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, ai.uid, ai.packageName) == MODE_ALLOWED; @@ -8016,6 +8042,13 @@ public class NotificationManagerService extends SystemService { return false; } + if (Flags.rejectOldNotifications() && n.hasAppProvidedWhen() && n.getWhen() > 0 + && (System.currentTimeMillis() - n.getWhen()) > NOTIFICATION_MAX_AGE_AT_POST) { + Slog.d(TAG, "Ignored enqueue for old " + n.getWhen() + " notification " + r.getKey()); + mUsageStats.registerTooOldBlocked(r); + return false; + } + return true; } diff --git a/services/core/java/com/android/server/notification/NotificationUsageStats.java b/services/core/java/com/android/server/notification/NotificationUsageStats.java index e960f4ba11fd..c09077e349fd 100644 --- a/services/core/java/com/android/server/notification/NotificationUsageStats.java +++ b/services/core/java/com/android/server/notification/NotificationUsageStats.java @@ -257,6 +257,14 @@ public class NotificationUsageStats { } } + public synchronized void registerTooOldBlocked(NotificationRecord notification) { + AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification); + for (AggregatedStats stats : aggregatedStatsArray) { + stats.numTooOld++; + } + releaseAggregatedStatsLocked(aggregatedStatsArray); + } + @GuardedBy("this") private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) { return getAggregatedStatsLocked(record.getSbn().getPackageName()); @@ -405,6 +413,7 @@ public class NotificationUsageStats { public int numUndecoratedRemoteViews; public long mLastAccessTime; public int numImagesRemoved; + public int numTooOld; public AggregatedStats(Context context, String key) { this.key = key; @@ -535,6 +544,7 @@ public class NotificationUsageStats { maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations)); maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations)); maybeCount("note_images_removed", (numImagesRemoved - previous.numImagesRemoved)); + maybeCount("not_too_old", (numTooOld - previous.numTooOld)); noisyImportance.maybeCount(previous.noisyImportance); quietImportance.maybeCount(previous.quietImportance); finalImportance.maybeCount(previous.finalImportance); @@ -570,6 +580,7 @@ public class NotificationUsageStats { previous.numAlertViolations = numAlertViolations; previous.numQuotaViolations = numQuotaViolations; previous.numImagesRemoved = numImagesRemoved; + previous.numTooOld = numTooOld; noisyImportance.update(previous.noisyImportance); quietImportance.update(previous.quietImportance); finalImportance.update(previous.finalImportance); @@ -679,6 +690,8 @@ public class NotificationUsageStats { output.append("numQuotaViolations=").append(numQuotaViolations).append("\n"); output.append(indentPlusTwo); output.append("numImagesRemoved=").append(numImagesRemoved).append("\n"); + output.append(indentPlusTwo); + output.append("numTooOld=").append(numTooOld).append("\n"); output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n"); output.append(indentPlusTwo).append(quietImportance.toString()).append("\n"); output.append(indentPlusTwo).append(finalImportance.toString()).append("\n"); @@ -725,6 +738,7 @@ public class NotificationUsageStats { maybePut(dump, "notificationEnqueueRate", getEnqueueRate()); maybePut(dump, "numAlertViolations", numAlertViolations); maybePut(dump, "numImagesRemoved", numImagesRemoved); + maybePut(dump, "numTooOld", numTooOld); noisyImportance.maybePut(dump, previous.noisyImportance); quietImportance.maybePut(dump, previous.quietImportance); finalImportance.maybePut(dump, previous.finalImportance); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 9dcca494ca24..bf6b6521c19a 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -135,3 +135,10 @@ flag { description: "This flag controls which signal is used to handle a user switch system event" bug: "337077643" } + +flag { + name: "reject_old_notifications" + namespace: "systemui" + description: "This flag does not allow notifications older than 2 weeks old to be posted" + bug: "339833083" +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 99401a17f83c..235e3cd7c9d2 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -16,6 +16,10 @@ package com.android.server.ondeviceintelligence; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_LOADED_BUNDLE_KEY; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.MODEL_UNLOADED_BUNDLE_KEY; +import static android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService.REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY; + import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeInferenceParams; import static com.android.server.ondeviceintelligence.BundleUtil.validatePfdReadOnly; import static com.android.server.ondeviceintelligence.BundleUtil.sanitizeStateParams; @@ -41,6 +45,7 @@ import android.app.ondeviceintelligence.ITokenInfoCallback; import android.app.ondeviceintelligence.OnDeviceIntelligenceException; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.res.Resources; @@ -105,12 +110,20 @@ public class OnDeviceIntelligenceManagerService extends SystemService { /** Handler message to {@link #resetTemporaryServices()} */ private static final int MSG_RESET_TEMPORARY_SERVICE = 0; + /** Handler message to clean up temporary broadcast keys. */ + private static final int MSG_RESET_BROADCAST_KEYS = 1; + /** Default value in absence of {@link DeviceConfig} override. */ private static final boolean DEFAULT_SERVICE_ENABLED = true; private static final String NAMESPACE_ON_DEVICE_INTELLIGENCE = "ondeviceintelligence"; + private static final String SYSTEM_PACKAGE = "android"; + + private final Executor resourceClosingExecutor = Executors.newCachedThreadPool(); private final Executor callbackExecutor = Executors.newCachedThreadPool(); + private final Executor broadcastExecutor = Executors.newCachedThreadPool(); + private final Context mContext; protected final Object mLock = new Object(); @@ -123,10 +136,14 @@ public class OnDeviceIntelligenceManagerService extends SystemService { @GuardedBy("mLock") private String[] mTemporaryServiceNames; + @GuardedBy("mLock") + private String[] mTemporaryBroadcastKeys; + @GuardedBy("mLock") + private String mBroadcastPackageName; + /** * Handler used to reset the temporary service names. */ - @GuardedBy("mLock") private Handler mTemporaryHandler; public OnDeviceIntelligenceManagerService(Context context) { @@ -482,6 +499,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { ensureRemoteIntelligenceServiceInitialized(); mRemoteOnDeviceIntelligenceService.run( IOnDeviceIntelligenceService::notifyInferenceServiceConnected); + broadcastExecutor.execute( + () -> registerModelLoadingBroadcasts(service)); service.registerRemoteStorageService( getIRemoteStorageService()); } catch (RemoteException ex) { @@ -493,6 +512,56 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } } + private void registerModelLoadingBroadcasts(IOnDeviceSandboxedInferenceService service) { + String[] modelBroadcastKeys; + try { + modelBroadcastKeys = getBroadcastKeys(); + } catch (Resources.NotFoundException e) { + Slog.d(TAG, "Skipping model broadcasts as broadcast intents configured."); + return; + } + + Bundle bundle = new Bundle(); + bundle.putBoolean(REGISTER_MODEL_UPDATE_CALLBACK_BUNDLE_KEY, true); + try { + service.updateProcessingState(bundle, new IProcessingUpdateStatusCallback.Stub() { + @Override + public void onSuccess(PersistableBundle statusParams) { + Binder.clearCallingIdentity(); + synchronized (mLock) { + if (statusParams.containsKey(MODEL_LOADED_BUNDLE_KEY)) { + String modelLoadedBroadcastKey = modelBroadcastKeys[0]; + if (modelLoadedBroadcastKey != null + && !modelLoadedBroadcastKey.isEmpty()) { + final Intent intent = new Intent(modelLoadedBroadcastKey); + intent.setPackage(mBroadcastPackageName); + mContext.sendBroadcast(intent, + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE); + } + } else if (statusParams.containsKey(MODEL_UNLOADED_BUNDLE_KEY)) { + String modelUnloadedBroadcastKey = modelBroadcastKeys[1]; + if (modelUnloadedBroadcastKey != null + && !modelUnloadedBroadcastKey.isEmpty()) { + final Intent intent = new Intent(modelUnloadedBroadcastKey); + intent.setPackage(mBroadcastPackageName); + mContext.sendBroadcast(intent, + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE); + } + } + } + } + + @Override + public void onFailure(int errorCode, String errorMessage) { + Slog.e(TAG, "Failed to register model loading callback with status code", + new OnDeviceIntelligenceException(errorCode, errorMessage)); + } + }); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register model loading callback with status code", e); + } + } + @NonNull private IRemoteStorageService.Stub getIRemoteStorageService() { return new IRemoteStorageService.Stub() { @@ -629,6 +698,20 @@ public class OnDeviceIntelligenceManagerService extends SystemService { R.string.config_defaultOnDeviceSandboxedInferenceService)}; } + protected String[] getBroadcastKeys() throws Resources.NotFoundException { + // TODO 329240495 : Consider a small class with explicit field names for the two services + synchronized (mLock) { + if (mTemporaryBroadcastKeys != null && mTemporaryBroadcastKeys.length == 2) { + return mTemporaryBroadcastKeys; + } + } + + return new String[]{mContext.getResources().getString( + R.string.config_onDeviceIntelligenceModelLoadedBroadcastKey), + mContext.getResources().getString( + R.string.config_onDeviceIntelligenceModelUnloadedBroadcastKey)}; + } + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public void setTemporaryServices(@NonNull String[] componentNames, int durationMs) { Objects.requireNonNull(componentNames); @@ -645,25 +728,26 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteOnDeviceIntelligenceService.unbind(); mRemoteOnDeviceIntelligenceService = null; } - if (mTemporaryHandler == null) { - mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { - synchronized (mLock) { - resetTemporaryServices(); - } - } else { - Slog.wtf(TAG, "invalid handler msg: " + msg); - } - } - }; - } else { - mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + + if (durationMs != -1) { + getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, + durationMs); } + } + } + @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + public void setModelBroadcastKeys(@NonNull String[] broadcastKeys, String receiverPackageName, + int durationMs) { + Objects.requireNonNull(broadcastKeys); + enforceShellOnly(Binder.getCallingUid(), "setModelBroadcastKeys"); + mContext.enforceCallingPermission( + Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); + synchronized (mLock) { + mTemporaryBroadcastKeys = broadcastKeys; + mBroadcastPackageName = receiverPackageName; if (durationMs != -1) { - mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs); + getTemporaryHandler().sendEmptyMessageDelayed(MSG_RESET_BROADCAST_KEYS, durationMs); } } } @@ -751,4 +835,28 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } } } + + private synchronized Handler getTemporaryHandler() { + if (mTemporaryHandler == null) { + mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { + synchronized (mLock) { + resetTemporaryServices(); + } + } else if (msg.what == MSG_RESET_BROADCAST_KEYS) { + synchronized (mLock) { + mTemporaryBroadcastKeys = null; + mBroadcastPackageName = SYSTEM_PACKAGE; + } + } else { + Slog.wtf(TAG, "invalid handler msg: " + msg); + } + } + }; + } + + return mTemporaryHandler; + } } diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java index a76d8a31405d..5744b5c3c2c4 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java @@ -43,6 +43,8 @@ final class OnDeviceIntelligenceShellCommand extends ShellCommand { return setTemporaryServices(); case "get-services": return getConfiguredServices(); + case "set-model-broadcasts": + return setBroadcastKeys(); default: return handleDefaultCommands(cmd); } @@ -62,12 +64,18 @@ final class OnDeviceIntelligenceShellCommand extends ShellCommand { pw.println(" To reset, call without any arguments."); pw.println(" get-services To get the names of services that are currently being used."); + pw.println( + " set-model-broadcasts [ModelLoadedBroadcastKey] [ModelUnloadedBroadcastKey] " + + "[ReceiverPackageName] " + + "[DURATION] To set the names of broadcast intent keys that are to be " + + "emitted for cts tests."); } private int setTemporaryServices() { final PrintWriter out = getOutPrintWriter(); final String intelligenceServiceName = getNextArg(); final String inferenceServiceName = getNextArg(); + if (getRemainingArgsCount() == 0 && intelligenceServiceName == null && inferenceServiceName == null) { mService.resetTemporaryServices(); @@ -79,7 +87,8 @@ final class OnDeviceIntelligenceShellCommand extends ShellCommand { Objects.requireNonNull(inferenceServiceName); final int duration = Integer.parseInt(getNextArgRequired()); mService.setTemporaryServices( - new String[]{intelligenceServiceName, inferenceServiceName}, duration); + new String[]{intelligenceServiceName, inferenceServiceName}, + duration); out.println("OnDeviceIntelligenceService temporarily set to " + intelligenceServiceName + " \n and \n OnDeviceTrustedInferenceService set to " + inferenceServiceName + " for " + duration + "ms"); @@ -93,4 +102,22 @@ final class OnDeviceIntelligenceShellCommand extends ShellCommand { + " \n and \n OnDeviceTrustedInferenceService set to : " + services[1]); return 0; } + + private int setBroadcastKeys() { + final PrintWriter out = getOutPrintWriter(); + final String modelLoadedKey = getNextArgRequired(); + final String modelUnloadedKey = getNextArgRequired(); + final String receiverPackageName = getNextArg(); + + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setModelBroadcastKeys( + new String[]{modelLoadedKey, modelUnloadedKey}, receiverPackageName, duration); + out.println("OnDeviceIntelligence Model Loading broadcast keys temporarily set to " + + modelLoadedKey + + " \n and \n OnDeviceTrustedInferenceService set to " + modelUnloadedKey + + "\n and Package name set to : " + receiverPackageName + + " for " + duration + "ms"); + return 0; + } + }
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index 9ba88aa18ce6..fe774aa75efc 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -504,9 +504,12 @@ public class AppDataHelper { } else { storageFlags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE; } - List<String> deferPackages = reconcileAppsDataLI(StorageManager.UUID_PRIVATE_INTERNAL, - UserHandle.USER_SYSTEM, storageFlags, true /* migrateAppData */, - true /* onlyCoreApps */); + final List<String> deferPackages; + synchronized (mPm.mInstallLock) { + deferPackages = reconcileAppsDataLI(StorageManager.UUID_PRIVATE_INTERNAL, + UserHandle.USER_SYSTEM, storageFlags, true /* migrateAppData */, + true /* onlyCoreApps */); + } Future<?> prepareAppDataFuture = SystemServerInitThreadPool.submit(() -> { TimingsTraceLog traceLog = new TimingsTraceLog("SystemServerTimingAsync", Trace.TRACE_TAG_PACKAGE_MANAGER); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 9e16b8abe0de..57827c567b5b 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -989,6 +989,10 @@ final class LetterboxUiController { } } + boolean isLetterboxEducationEnabled() { + return mLetterboxConfiguration.getIsEducationEnabled(); + } + /** * Whether we use split screen aspect ratio for the activity when camera compat treatment * is active because the corresponding config is enabled and activity supports resizing. diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 8bd7b5f78cf4..8defec3dbeab 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3448,6 +3448,8 @@ class Task extends TaskFragment { // Whether the direct top activity is eligible for letterbox education. appCompatTaskInfo.topActivityEligibleForLetterboxEducation = isTopActivityResumed && top.isEligibleForLetterboxEducation(); + appCompatTaskInfo.isLetterboxEducationEnabled = top != null + && top.mLetterboxUiController.isLetterboxEducationEnabled(); // Whether the direct top activity requested showing camera compat control. appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = isTopActivityResumed ? top.getCameraCompatControlState() diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 9e4f8219ea96..d3072000a56e 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -1276,7 +1276,23 @@ class AppIdPermissionPolicy : SchemePolicy() { packageName, permissionName ) - else -> permissionAllowlist.getSignatureAppAllowlistState(packageName, permissionName) + else -> + permissionAllowlist.getProductSignatureAppAllowlistState( + packageName, + permissionName + ) + ?: permissionAllowlist.getVendorSignatureAppAllowlistState( + packageName, + permissionName + ) + ?: permissionAllowlist.getSystemExtSignatureAppAllowlistState( + packageName, + permissionName + ) + ?: permissionAllowlist.getSignatureAppAllowlistState( + packageName, + permissionName + ) } } diff --git a/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java b/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java index 6a82f1656414..3e87c6fe7be7 100644 --- a/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java +++ b/services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -543,6 +544,18 @@ public class TransportConnectionTest { return future.get(); } + @Test + public void onBindingDied_referenceLost_doesNotThrow() { + TransportConnection.TransportConnectionMonitor transportConnectionMonitor = + new TransportConnection.TransportConnectionMonitor( + mContext, /* transportConnection= */ null); + doThrow(new IllegalArgumentException("Service not registered")).when( + mContext).unbindService(any()); + + // Test no exception is thrown + transportConnectionMonitor.onBindingDied(mTransportComponent); + } + private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) { ArgumentCaptor<ServiceConnection> connectionCaptor = ArgumentCaptor.forClass(ServiceConnection.class); diff --git a/services/tests/apexsystemservices/OWNERS b/services/tests/apexsystemservices/OWNERS index 0295b9e99326..8b6675ad22d7 100644 --- a/services/tests/apexsystemservices/OWNERS +++ b/services/tests/apexsystemservices/OWNERS @@ -1,4 +1 @@ -omakoto@google.com -satayev@google.com - include platform/packages/modules/common:/OWNERS diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 2d672b89662f..200952c05610 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -112,6 +112,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; +import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS; import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION; import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; import static com.android.server.notification.NotificationManagerService.NOTIFICATION_TTL; @@ -339,6 +340,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -909,7 +911,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } mService.clearNotifications(); - TestableLooper.get(this).processAllMessages(); + if (mTestableLooper != null) { + mTestableLooper.processAllMessages(); + } try { mService.onDestroy(); @@ -920,14 +924,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { InstrumentationRegistry.getInstrumentation() .getUiAutomation().dropShellPermissionIdentity(); - // Remove scheduled messages that would be processed when the test is already done, and - // could cause issues, for example, messages that remove/cancel shown toasts (this causes - // problematic interactions with mocks when they're no longer working as expected). - mWorkerHandler.removeCallbacksAndMessages(null); + if (mWorkerHandler != null) { + // Remove scheduled messages that would be processed when the test is already done, and + // could cause issues, for example, messages that remove/cancel shown toasts (this causes + // problematic interactions with mocks when they're no longer working as expected). + mWorkerHandler.removeCallbacksAndMessages(null); + } - if (TestableLooper.get(this) != null) { + if (mTestableLooper != null) { // Must remove static reference to this test object to prevent leak (b/261039202) - TestableLooper.remove(this); + mTestableLooper.remove(this); } } @@ -1009,7 +1015,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } public void waitForIdle() { - mTestableLooper.processAllMessages(); + if (mTestableLooper != null) { + mTestableLooper.processAllMessages(); + } } private void setUpPrefsForBubbles(String pkg, int uid, boolean globalEnabled, @@ -1302,6 +1310,106 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { return nrSummary; } + private NotificationRecord createAndPostCallStyleNotification(String packageName, + UserHandle userHandle, String testName) throws Exception { + Person person = new Person.Builder().setName("caller").build(); + Notification.Builder nb = new Notification.Builder(mContext, + mTestNotificationChannel.getId()) + .setFlag(FLAG_USER_INITIATED_JOB, true) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) + .setSmallIcon(android.R.drawable.sym_def_app_icon); + StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, 1, + testName, mUid, 0, nb.build(), userHandle, null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mService.addEnqueuedNotification(r); + mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)).run(); + waitForIdle(); + + return mService.findNotificationLocked( + packageName, r.getSbn().getTag(), r.getSbn().getId(), r.getSbn().getUserId()); + } + + private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName) + throws RemoteException { + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, testName, mUid, 0, + nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(), + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + + return mService.findNotificationLocked( + mPkg, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId()); + } + + private static <T extends Parcelable> T parcelAndUnparcel(T source, + Parcelable.Creator<T> creator) { + Parcel parcel = Parcel.obtain(); + source.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return creator.createFromParcel(parcel); + } + + private PendingIntent createPendingIntent(String action) { + return PendingIntent.getActivity(mContext, 0, + new Intent(action).setPackage(mContext.getPackageName()), + PendingIntent.FLAG_MUTABLE); + } + + private void allowTestPackageToToast() throws Exception { + assertWithMessage("toast queue").that(mService.mToastQueue).isEmpty(); + mService.isSystemUid = false; + mService.isSystemAppId = false; + setToastRateIsWithinQuota(true); + setIfPackageHasPermissionToAvoidToastRateLimiting(TEST_PACKAGE, false); + // package is not suspended + when(mPackageManager.isPackageSuspendedForUser(TEST_PACKAGE, mUserId)) + .thenReturn(false); + } + + private boolean enqueueToast(String testPackage, ITransientNotification callback) + throws RemoteException { + return enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(), + callback); + } + + private boolean enqueueToast(INotificationManager service, String testPackage, + IBinder token, ITransientNotification callback) throws RemoteException { + return service.enqueueToast(testPackage, token, callback, TOAST_DURATION, /* isUiContext= */ + true, DEFAULT_DISPLAY); + } + + private boolean enqueueTextToast(String testPackage, CharSequence text) throws RemoteException { + return enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY); + } + + private boolean enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext, + int displayId) throws RemoteException { + return ((INotificationManager) mService.mService).enqueueTextToast(testPackage, + new Binder(), text, TOAST_DURATION, isUiContext, displayId, + /* textCallback= */ null); + } + + private void mockIsVisibleBackgroundUsersSupported(boolean supported) { + when(mUm.isVisibleBackgroundUsersSupported()).thenReturn(supported); + } + + private void mockIsUserVisible(int displayId, boolean visible) { + when(mUmInternal.isUserVisible(mUserId, displayId)).thenReturn(visible); + } + + private void mockDisplayAssignedToUser(int displayId) { + when(mUmInternal.getMainDisplayAssignedToUser(mUserId)).thenReturn(displayId); + } + + private void verifyToastShownForTestPackage(String text, int displayId) { + verify(mStatusBar).showToast(eq(mUid), eq(TEST_PACKAGE), any(), eq(text), any(), + eq(TOAST_DURATION), any(), eq(displayId)); + } + @Test @DisableFlags(FLAG_ALL_NOTIFS_NEED_TTL) public void testLimitTimeOutBroadcast() { @@ -14069,11 +14177,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void enqueueUpdate_whenBelowMaxEnqueueRate_accepts() throws Exception { // Post the first version. Notification original = generateNotificationRecord(null).getNotification(); - original.when = 111; + original.when = System.currentTimeMillis(); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, original, mUserId); waitForIdle(); assertThat(mService.mNotificationList).hasSize(1); - assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111); + assertThat(mService.mNotificationList.get(0).getNotification().when) + .isEqualTo(original.when); reset(mUsageStats); when(mUsageStats.getAppEnqueueRate(eq(mPkg))) @@ -14081,7 +14190,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Post the update. Notification update = generateNotificationRecord(null).getNotification(); - update.when = 222; + update.when = System.currentTimeMillis() + 111; mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, update, mUserId); waitForIdle(); @@ -14090,18 +14199,19 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mUsageStats, never()).registerPostedByApp(any()); verify(mUsageStats).registerUpdatedByApp(any(), any()); assertThat(mService.mNotificationList).hasSize(1); - assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(222); + assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(update.when); } @Test public void enqueueUpdate_whenAboveMaxEnqueueRate_rejects() throws Exception { // Post the first version. Notification original = generateNotificationRecord(null).getNotification(); - original.when = 111; + original.when = System.currentTimeMillis(); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, original, mUserId); waitForIdle(); assertThat(mService.mNotificationList).hasSize(1); - assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111); + assertThat(mService.mNotificationList.get(0).getNotification().when) + .isEqualTo(original.when); reset(mUsageStats); when(mUsageStats.getAppEnqueueRate(eq(mPkg))) @@ -14109,7 +14219,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Post the update. Notification update = generateNotificationRecord(null).getNotification(); - update.when = 222; + update.when = System.currentTimeMillis() + 111; mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 0, update, mUserId); waitForIdle(); @@ -14118,7 +14228,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mUsageStats, never()).registerPostedByApp(any()); verify(mUsageStats, never()).registerUpdatedByApp(any(), any()); assertThat(mService.mNotificationList).hasSize(1); - assertThat(mService.mNotificationList.get(0).getNotification().when).isEqualTo(111); // old + assertThat(mService.mNotificationList.get(0).getNotification().when) + .isEqualTo(original.when); // old } @Test @@ -15483,103 +15594,48 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(n.getTimeoutAfter()).isEqualTo(20); } - private NotificationRecord createAndPostCallStyleNotification(String packageName, - UserHandle userHandle, String testName) throws Exception { - Person person = new Person.Builder().setName("caller").build(); - Notification.Builder nb = new Notification.Builder(mContext, - mTestNotificationChannel.getId()) - .setFlag(FLAG_USER_INITIATED_JOB, true) - .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) - .setSmallIcon(android.R.drawable.sym_def_app_icon); - StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, 1, - testName, mUid, 0, nb.build(), userHandle, null, 0); + @Test + @EnableFlags(FLAG_REJECT_OLD_NOTIFICATIONS) + public void testRejectOldNotification_oldWhen() throws Exception { + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setWhen(System.currentTimeMillis() - Duration.ofDays(15).toMillis()) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); - mService.addEnqueuedNotification(r); - mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), - r.getUid(), mPostNotificationTrackerFactory.newTracker(null)).run(); - waitForIdle(); - - return mService.findNotificationLocked( - packageName, r.getSbn().getTag(), r.getSbn().getId(), r.getSbn().getUserId()); - } - - private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName) - throws RemoteException { - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, testName, mUid, 0, - nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0); - NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); - - mBinderService.enqueueNotificationWithTag(mPkg, mPkg, sbn.getTag(), - nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); - waitForIdle(); - - return mService.findNotificationLocked( - mPkg, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId()); - } - - private static <T extends Parcelable> T parcelAndUnparcel(T source, - Parcelable.Creator<T> creator) { - Parcel parcel = Parcel.obtain(); - source.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - return creator.createFromParcel(parcel); - } - - private PendingIntent createPendingIntent(String action) { - return PendingIntent.getActivity(mContext, 0, - new Intent(action).setPackage(mContext.getPackageName()), - PendingIntent.FLAG_MUTABLE); - } - - private void allowTestPackageToToast() throws Exception { - assertWithMessage("toast queue").that(mService.mToastQueue).isEmpty(); - mService.isSystemUid = false; - mService.isSystemAppId = false; - setToastRateIsWithinQuota(true); - setIfPackageHasPermissionToAvoidToastRateLimiting(TEST_PACKAGE, false); - // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(TEST_PACKAGE, mUserId)) - .thenReturn(false); - } - - private boolean enqueueToast(String testPackage, ITransientNotification callback) - throws RemoteException { - return enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(), - callback); - } - - private boolean enqueueToast(INotificationManager service, String testPackage, - IBinder token, ITransientNotification callback) throws RemoteException { - return service.enqueueToast(testPackage, token, callback, TOAST_DURATION, /* isUiContext= */ - true, DEFAULT_DISPLAY); - } - - private boolean enqueueTextToast(String testPackage, CharSequence text) throws RemoteException { - return enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY); - } - - private boolean enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext, - int displayId) throws RemoteException { - return ((INotificationManager) mService.mService).enqueueTextToast(testPackage, - new Binder(), text, TOAST_DURATION, isUiContext, displayId, - /* textCallback= */ null); + assertThat(mService.checkDisqualifyingFeatures(mUserId, mUid, 0, null, r, false, false)) + .isFalse(); } - private void mockIsVisibleBackgroundUsersSupported(boolean supported) { - when(mUm.isVisibleBackgroundUsersSupported()).thenReturn(supported); - } + @Test + @EnableFlags(FLAG_REJECT_OLD_NOTIFICATIONS) + public void testRejectOldNotification_mediumOldWhen() throws Exception { + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setWhen(System.currentTimeMillis() - Duration.ofDays(13).toMillis()) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); - private void mockIsUserVisible(int displayId, boolean visible) { - when(mUmInternal.isUserVisible(mUserId, displayId)).thenReturn(visible); + assertThat(mService.checkDisqualifyingFeatures(mUserId, mUid, 0, null, r, false, false)) + .isTrue(); } - private void mockDisplayAssignedToUser(int displayId) { - when(mUmInternal.getMainDisplayAssignedToUser(mUserId)).thenReturn(displayId); - } + @Test + @EnableFlags(FLAG_REJECT_OLD_NOTIFICATIONS) + public void testRejectOldNotification_zeroWhen() throws Exception { + Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setWhen(0) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); - private void verifyToastShownForTestPackage(String text, int displayId) { - verify(mStatusBar).showToast(eq(mUid), eq(TEST_PACKAGE), any(), eq(text), any(), - eq(TOAST_DURATION), any(), eq(displayId)); + assertThat(mService.checkDisqualifyingFeatures(mUserId, mUid, 0, null, r, false, false)) + .isTrue(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index a60d243c9dad..1195c934a6f7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -1623,6 +1623,12 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertTrue(mController.allowHorizontalReachabilityForThinLetterbox()); } + @Test + public void testIsLetterboxEducationEnabled() { + mController.isLetterboxEducationEnabled(); + verify(mLetterboxConfiguration).getIsEducationEnabled(); + } + private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", /* className */ ""); |