summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/AppCompatTaskInfo.java9
-rw-r--r--core/java/android/app/AppOpsManager.java12
-rw-r--r--core/java/android/permission/flags.aconfig10
-rw-r--r--core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java15
-rw-r--r--core/java/android/window/TransitionFilter.java22
-rw-r--r--core/res/res/values/config.xml7
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt31
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt82
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt91
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt67
-rw-r--r--packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java2
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt217
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt35
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt17
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt25
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt19
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt6
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt19
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt25
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt229
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt79
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt68
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt70
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt1320
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt33
-rw-r--r--services/backup/java/com/android/server/backup/transport/TransportConnection.java14
-rw-r--r--services/core/java/com/android/server/appop/AppOpsService.java94
-rw-r--r--services/core/java/com/android/server/appop/AppOpsUidStateTracker.java1
-rw-r--r--services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java21
-rw-r--r--services/core/java/com/android/server/appop/AttributedOp.java14
-rw-r--r--services/core/java/com/android/server/audio/SpatializerHelper.java5
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java63
-rw-r--r--services/core/java/com/android/server/notification/NotificationUsageStats.java14
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig7
-rw-r--r--services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java142
-rw-r--r--services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceShellCommand.java29
-rw-r--r--services/core/java/com/android/server/pm/AppDataHelper.java9
-rw-r--r--services/core/java/com/android/server/wm/LetterboxUiController.java4
-rw-r--r--services/core/java/com/android/server/wm/Task.java2
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt18
-rw-r--r--services/robotests/backup/src/com/android/server/backup/transport/TransportConnectionTest.java13
-rw-r--r--services/tests/apexsystemservices/OWNERS3
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java268
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java6
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 */ "");