Merge "Only set SCHED_RESET_ON_FORK flag if the thread is using the default scheduling policy." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 53da338..d1c0c42 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4947,8 +4947,10 @@
method public void update(android.app.ActivityOptions);
field public static final String EXTRA_USAGE_TIME_REPORT = "android.activity.usage_time";
field public static final String EXTRA_USAGE_TIME_REPORT_PACKAGES = "android.usage_time_packages";
- field public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1; // 0x1
- field public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2
+ field @Deprecated @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1; // 0x1
+ field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3; // 0x3
+ field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4; // 0x4
+ field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2
field public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED = 0; // 0x0
}
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 0d183c7..6ab39b0 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -68,6 +68,8 @@
import android.window.SplashScreen;
import android.window.WindowContainerToken;
+import com.android.window.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -109,35 +111,64 @@
MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS,
MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE})
public @interface BackgroundActivityStartMode {}
+
/**
- * No explicit value chosen. The system will decide whether to grant privileges.
+ * The system determines whether to grant background activity start privileges. This is the
+ * default behavior if no explicit mode is specified.
*/
public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED = 0;
/**
- * Allow the {@link PendingIntent} to use the background activity start privileges.
+ * Grants the {@link PendingIntent} background activity start privileges.
+ *
+ * This behaves the same as {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOWED_ALWAYS}, except it
+ * does not grant background activity launch permissions based on the privileged permission
+ * <code>START_ACTIVITIES_FROM_BACKGROUND</code>.
+ *
+ * @deprecated Use {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE} to allow starts
+ * only when the app is visible or {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS} to
+ * allow starts at any time (see <a
+ * href="https://developer.android.com/guide/components/activities/background-starts">
+ * Restrictions on starting activities from the background</a>).
*/
+ @Deprecated
+ @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES)
public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1;
/**
- * Deny the {@link PendingIntent} to use the background activity start privileges.
+ * Denies the {@link PendingIntent} any background activity start privileges.
*/
+ @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES)
public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2;
/**
- * Allow the {@link PendingIntent} to use ALL background activity start privileges, including
- * special permissions that will allow starts at any time.
+ * Grants the {@link PendingIntent} all background activity start privileges, including
+ * those normally reserved for privileged contexts (e.g., companion apps or those with the
+ * {@code START_ACTIVITIES_FROM_BACKGROUND} permission).
*
- * @hide
+ * <p><b>Caution:</b> This mode should be used sparingly. Most apps should use
+ * {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE} instead, relying on notifications
+ * or foreground services for background interactions to minimize user disruption. However,
+ * this mode is necessary for specific use cases, such as companion apps responding to
+ * prompts from a connected device.
+ *
+ * <p>For more information on background activity start restrictions, see:
+ * <a href="https://developer.android.com/guide/components/activities/background-starts">
+ * Restrictions on starting activities from the background</a>
*/
+ @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES)
public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3;
/**
- * Allow the {@link PendingIntent} to use background activity start privileges based on
- * visibility of the app.
+ * Grants the {@link PendingIntent} background activity start privileges only when the app
+ * has a visible window (i.e., is visible to the user). This is the recommended mode for most
+ * apps to minimize disruption to the user experience.
*
- * @hide
+ * <p>For more information on background activity start restrictions, see:
+ * <a href="https://developer.android.com/guide/components/activities/background-starts">
+ * Restrictions on starting activities from the background</a>
*/
+ @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES)
public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4;
/**
- * Special behavior for compatibility.
- * Similar to {@link #MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED}
+ * Provides compatibility with previous Android versions regarding background activity starts.
+ * Similar to {@link #MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED}.
*
* @hide
*/
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 95d3ea5..0c02ba4 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -6285,7 +6285,7 @@
}
r.activity.mConfigChangeFlags |= configChanges;
- r.mPreserveWindow = tmp.mPreserveWindow;
+ r.mPreserveWindow = r.activity.mWindowAdded && tmp.mPreserveWindow;
r.activity.mChangingConfigurations = true;
diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
index b9eba9c..ce8661e 100644
--- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
@@ -1028,6 +1028,9 @@
// Camera is already closed, so nothing left to do
if (DEBUG) Log.v(TAG, mIdString +
"Camera was already closed or busy, skipping unconfigure");
+ } catch (SecurityException e) {
+ // UID state change revoked camera permission
+ Log.e(TAG, mIdString + "Exception while unconfiguring outputs: ", e);
}
}
}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 880f30c..2e72f0e 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -585,6 +585,8 @@
<permission name="android.permission.EXECUTE_APP_FUNCTIONS" />
<!-- Permission required for CTS test - CtsNfcTestCases -->
<permission name="android.permission.NFC_SET_CONTROLLER_ALWAYS_ON" />
+ <!-- Permission required for CTS test - CtsAppTestCases -->
+ <permission name="android.permission.KILL_UID" />
</privapp-permissions>
<privapp-permissions package="com.android.statementservice">
diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
index 07e5ac1..b74d922 100644
--- a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
+++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
@@ -22,6 +22,6 @@
android:viewportHeight="960"
android:viewportWidth="960">
<path
- android:fillColor="@android:color/system_on_tertiary_fixed"
+ android:fillColor="@android:color/system_on_tertiary_container_light"
android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" />
</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
index bdee883..09a049c 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
@@ -37,7 +37,7 @@
android:layout_marginStart="2dp"
android:lineHeight="20dp"
android:maxWidth="150dp"
- android:textColor="@android:color/system_on_tertiary_fixed"
+ android:textColor="@android:color/system_on_tertiary_container_light"
android:textFontWeight="500"
android:textSize="14sp" />
</LinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 0dca97c..e80b95b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -65,6 +65,7 @@
import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
@@ -73,6 +74,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
+import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
@@ -90,6 +92,7 @@
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
+import com.android.wm.shell.freeform.TaskChangeListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarterInitializer;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
@@ -384,9 +387,12 @@
Context context,
ShellInit shellInit,
Transitions transitions,
- WindowDecorViewModel windowDecorViewModel) {
+ Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
+ WindowDecorViewModel windowDecorViewModel,
+ Optional<TaskChangeListener> taskChangeListener) {
return new FreeformTaskTransitionObserver(
- context, shellInit, transitions, windowDecorViewModel);
+ context, shellInit, transitions, desktopImmersiveTransitionHandler,
+ windowDecorViewModel, taskChangeListener);
}
@WMSingleton
@@ -410,7 +416,6 @@
// One handed mode
//
-
// Needs the shell main handler for ContentObserver callbacks
@WMSingleton
@Provides
@@ -621,6 +626,7 @@
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
@DynamicOverride DesktopRepository desktopRepository,
+ Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler,
DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
RecentsTransitionHandler recentsTransitionHandler,
@@ -636,7 +642,8 @@
returnToDragStartAnimator, enterDesktopTransitionHandler,
exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler,
toggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler, desktopRepository,
+ dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(),
+ desktopRepository,
desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
recentTasksController.orElse(null), interactionJankMonitor, mainHandler);
@@ -644,6 +651,15 @@
@WMSingleton
@Provides
+ static Optional<TaskChangeListener> provideDesktopTaskChangeListener(Context context) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ return Optional.of(new DesktopTaskChangeListener());
+ }
+ return Optional.empty();
+ }
+
+ @WMSingleton
+ @Provides
static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter(
Context context,
Transitions transitions,
@@ -671,6 +687,19 @@
@WMSingleton
@Provides
+ static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
+ Context context,
+ Transitions transitions,
+ @DynamicOverride DesktopRepository desktopRepository) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ return Optional.of(
+ new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
+ }
+ return Optional.empty();
+ }
+
+ @WMSingleton
+ @Provides
static ReturnToDragStartAnimator provideReturnToDragStartAnimator(
Context context, InteractionJankMonitor interactionJankMonitor) {
return new ReturnToDragStartAnimator(context, interactionJankMonitor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
new file mode 100644
index 0000000..f749aa1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -0,0 +1,245 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.animation.DecelerateInterpolator
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import androidx.core.animation.addListener
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
+
+/**
+ * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task
+ * remains freeform while being able to take fullscreen bounds and have its App Header visibility
+ * be transient below the status bar like in fullscreen immersive mode.
+ */
+class DesktopFullImmersiveTransitionHandler(
+ private val transitions: Transitions,
+ private val desktopRepository: DesktopRepository,
+ private val transactionSupplier: () -> SurfaceControl.Transaction,
+) : TransitionHandler {
+
+ constructor(
+ transitions: Transitions,
+ desktopRepository: DesktopRepository,
+ ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
+
+ private var state: TransitionState? = null
+
+ /** Whether there is an immersive transition that hasn't completed yet. */
+ private val inProgress: Boolean
+ get() = state != null
+
+ private val rectEvaluator = RectEvaluator()
+
+ /** A listener to invoke on animation changes during entry/exit. */
+ var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
+
+ /** Starts a transition to enter full immersive state inside the desktop. */
+ fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ if (inProgress) {
+ ProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "FullImmersive: cannot start entry because transition already in progress."
+ )
+ return
+ }
+
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
+ state = TransitionState(
+ transition = transition,
+ displayId = taskInfo.displayId,
+ taskId = taskInfo.taskId,
+ direction = Direction.ENTER
+ )
+ }
+
+ fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ if (inProgress) {
+ ProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "$TAG: cannot start exit because transition already in progress."
+ )
+ return
+ }
+
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
+ state = TransitionState(
+ transition = transition,
+ displayId = taskInfo.displayId,
+ taskId = taskInfo.taskId,
+ direction = Direction.EXIT
+ )
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback
+ ): Boolean {
+ val state = requireState()
+ if (transition != state.transition) return false
+ animateResize(
+ transitionState = state,
+ info = info,
+ startTransaction = startTransaction,
+ finishTransaction = finishTransaction,
+ finishCallback = finishCallback
+ )
+ return true
+ }
+
+ private fun animateResize(
+ transitionState: TransitionState,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback
+ ) {
+ val change = info.changes.first { c ->
+ val taskInfo = c.taskInfo
+ return@first taskInfo != null && taskInfo.taskId == transitionState.taskId
+ }
+ val leash = change.leash
+ val startBounds = change.startAbsBounds
+ val endBounds = change.endAbsBounds
+
+ val updateTransaction = transactionSupplier()
+ ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply {
+ duration = FULL_IMMERSIVE_ANIM_DURATION_MS
+ interpolator = DecelerateInterpolator()
+ addListener(
+ onStart = {
+ startTransaction
+ .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
+ .setWindowCrop(leash, startBounds.width(), startBounds.height())
+ .show(leash)
+ onTaskResizeAnimationListener
+ ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds)
+ ?: startTransaction.apply()
+ },
+ onEnd = {
+ finishTransaction
+ .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat())
+ .setWindowCrop(leash, endBounds.width(), endBounds.height())
+ .apply()
+ onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId)
+ finishCallback.onTransitionFinished(null /* wct */)
+ clearState()
+ }
+ )
+ addUpdateListener { animation ->
+ val rect = animation.animatedValue as Rect
+ updateTransaction
+ .setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
+ .setWindowCrop(leash, rect.width(), rect.height())
+ .apply()
+ onTaskResizeAnimationListener
+ ?.onBoundsChange(transitionState.taskId, updateTransaction, rect)
+ ?: updateTransaction.apply()
+ }
+ start()
+ }
+ }
+
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo
+ ): WindowContainerTransaction? = null
+
+ override fun onTransitionConsumed(
+ transition: IBinder,
+ aborted: Boolean,
+ finishTransaction: SurfaceControl.Transaction?
+ ) {
+ val state = this.state ?: return
+ if (transition == state.transition && aborted) {
+ clearState()
+ }
+ super.onTransitionConsumed(transition, aborted, finishTransaction)
+ }
+
+ /**
+ * Called when any transition in the system is ready to play. This is needed to update the
+ * repository state before window decorations are drawn (which happens immediately after
+ * |onTransitionReady|, before this transition actually animates) because drawing decorations
+ * depends in whether the task is in full immersive state or not.
+ */
+ fun onTransitionReady(transition: IBinder) {
+ val state = this.state ?: return
+ // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
+ // immersive, which isn't realistic. The app could crash, the user could dismiss it from
+ // overview, etc. This (or its caller) should search all transitions to look for any
+ // immersive task exiting that state to keep the repository properly updated.
+ if (transition == state.transition) {
+ when (state.direction) {
+ Direction.ENTER -> {
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = state.displayId,
+ taskId = state.taskId,
+ immersive = true
+ )
+ }
+ Direction.EXIT -> {
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = state.displayId,
+ taskId = state.taskId,
+ immersive = false
+ )
+ }
+ }
+ }
+ }
+
+ private fun clearState() {
+ state = null
+ }
+
+ private fun requireState(): TransitionState =
+ state ?: error("Expected non-null transition state")
+
+ /** The state of the currently running transition. */
+ private data class TransitionState(
+ val transition: IBinder,
+ val displayId: Int,
+ val taskId: Int,
+ val direction: Direction
+ )
+
+ private enum class Direction {
+ ENTER, EXIT
+ }
+
+ private companion object {
+ private const val TAG = "FullImmersiveHandler"
+
+ private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
new file mode 100644
index 0000000..1ee2de9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import com.android.wm.shell.freeform.TaskChangeListener
+
+/** Manages tasks handling specific to Android Desktop Mode. */
+class DesktopTaskChangeListener: TaskChangeListener {
+
+ override fun onTaskOpening(taskInfo: RunningTaskInfo) {
+ // TODO: b/367268953 - Connect this with DesktopRepository.
+ }
+
+ override fun onTaskChanging(taskInfo: RunningTaskInfo) {
+ // TODO: b/367268953 - Connect this with DesktopRepository.
+ }
+
+ override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) {
+ // TODO: b/367268953 - Connect this with DesktopRepository.
+ }
+
+ override fun onTaskMovingToBack(taskInfo: RunningTaskInfo) {
+ // TODO: b/367268953 - Connect this with DesktopRepository.
+ }
+
+ override fun onTaskClosing(taskInfo: RunningTaskInfo) {
+ // TODO: b/367268953 - Connect this with DesktopRepository.
+ }
+}
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 2303e71..4e548a6 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
@@ -134,6 +134,7 @@
private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
+ private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler,
private val taskRepository: DesktopRepository,
private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
private val launchAdjacentController: LaunchAdjacentController,
@@ -231,6 +232,7 @@
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
+ immersiveTransitionHandler.onTaskResizeAnimationListener = listener
}
fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -649,6 +651,35 @@
}
}
+ /** Moves a task in/out of full immersive state within the desktop. */
+ fun toggleDesktopTaskFullImmersiveState(taskInfo: RunningTaskInfo) {
+ if (taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
+ exitDesktopTaskFromFullImmersive(taskInfo)
+ } else {
+ moveDesktopTaskToFullImmersive(taskInfo)
+ }
+ }
+
+ private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
+ check(taskInfo.isFreeform) { "Task must already be in freeform" }
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, Rect())
+ }
+ immersiveTransitionHandler.enterImmersive(taskInfo, wct)
+ }
+
+ private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
+ check(taskInfo.isFreeform) { "Task must already be in freeform" }
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
+ val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)
+
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, destinationBounds)
+ }
+ immersiveTransitionHandler.exitImmersive(taskInfo, wct)
+ }
+
/**
* Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
* bounds) and a free floating state (either the last saved bounds if available or the default
@@ -685,18 +716,7 @@
// and toggle to the stable bounds.
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
- if (taskInfo.isResizeable) {
- // if resizable then expand to entire stable bounds (full display minus insets)
- destinationBounds.set(stableBounds)
- } else {
- // if non-resizable then calculate max bounds according to aspect ratio
- val activityAspectRatio = calculateAspectRatio(taskInfo)
- val newSize = maximizeSizeGivenAspectRatio(taskInfo,
- Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
- val newBounds = centerInArea(
- newSize, stableBounds, stableBounds.left, stableBounds.top)
- destinationBounds.set(newBounds)
- }
+ destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
}
@@ -719,6 +739,20 @@
}
}
+ private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
+ if (taskInfo.isResizeable) {
+ // if resizable then expand to entire stable bounds (full display minus insets)
+ return Rect(stableBounds)
+ } else {
+ // if non-resizable then calculate max bounds according to aspect ratio
+ val activityAspectRatio = calculateAspectRatio(taskInfo)
+ val newSize = maximizeSizeGivenAspectRatio(taskInfo,
+ Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
+ return centerInArea(
+ newSize, stableBounds, stableBounds.left, stableBounds.top)
+ }
+ }
+
private fun isTaskMaximized(
taskInfo: RunningTaskInfo,
stableBounds: Rect
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index 68a250d..334dc5a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -23,6 +23,7 @@
import android.graphics.Point
import android.os.SystemProperties
import android.util.Slog
+import androidx.core.content.withStyledAttributes
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.desktopmode.CaptionState
@@ -32,8 +33,11 @@
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.EducationViewConfig
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainCoroutineDispatcher
@@ -70,6 +74,7 @@
@ShellMainThread private val applicationCoroutineScope: CoroutineScope,
@ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher,
) {
+ private val decorThemeUtil = DecorThemeUtil(context)
private lateinit var openHandleMenuCallback: (Int) -> Unit
private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit
@@ -97,7 +102,9 @@
}
.flowOn(backgroundDispatcher)
.collectLatest { captionState ->
- showEducation(captionState)
+ val tooltipColorScheme = tooltipColorScheme(captionState)
+
+ showEducation(captionState, tooltipColorScheme)
// After showing first tooltip, mark education as viewed
appHandleEducationDatastoreRepository.updateEducationViewedTimestampMillis(true)
}
@@ -123,7 +130,7 @@
if (canEnterDesktopMode(context) && Flags.enableDesktopWindowingAppHandleEducation()) block()
}
- private fun showEducation(captionState: CaptionState) {
+ private fun showEducation(captionState: CaptionState, tooltipColorScheme: TooltipColorScheme) {
val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds
val tooltipGlobalCoordinates =
Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom)
@@ -132,14 +139,17 @@
val appHandleTooltipConfig =
EducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip,
+ tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
tooltipText = getString(R.string.windowing_app_handle_education_tooltip),
arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP,
onEducationClickAction = {
- launchWithExceptionHandling { showWindowingImageButtonTooltip() }
+ launchWithExceptionHandling { showWindowingImageButtonTooltip(tooltipColorScheme) }
openHandleMenuCallback(captionState.runningTaskInfo.taskId)
},
- onDismissAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } },
+ onDismissAction = {
+ launchWithExceptionHandling { showWindowingImageButtonTooltip(tooltipColorScheme) }
+ },
)
windowingEducationViewController.showEducationTooltip(
@@ -147,7 +157,7 @@
}
/** Show tooltip that points to windowing image button in app handle menu */
- private suspend fun showWindowingImageButtonTooltip() {
+ private suspend fun showWindowingImageButtonTooltip(tooltipColorScheme: TooltipColorScheme) {
val appInfoPillHeight = getSize(R.dimen.desktop_mode_handle_menu_app_info_pill_height)
val windowingOptionPillHeight = getSize(R.dimen.desktop_mode_handle_menu_windowing_pill_height)
val appHandleMenuWidth =
@@ -188,18 +198,21 @@
val windowingImageButtonTooltipConfig =
EducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
+ tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
tooltipText =
getString(R.string.windowing_desktop_mode_image_button_education_tooltip),
arrowDirection =
DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
onEducationClickAction = {
- launchWithExceptionHandling { showExitWindowingTooltip() }
+ launchWithExceptionHandling { showExitWindowingTooltip(tooltipColorScheme) }
toDesktopModeCallback(
captionState.runningTaskInfo.taskId,
DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
},
- onDismissAction = { launchWithExceptionHandling { showExitWindowingTooltip() } },
+ onDismissAction = {
+ launchWithExceptionHandling { showExitWindowingTooltip(tooltipColorScheme) }
+ },
)
windowingEducationViewController.showEducationTooltip(
@@ -209,7 +222,7 @@
}
/** Show tooltip that points to app chip button and educates user on how to exit desktop mode */
- private suspend fun showExitWindowingTooltip() {
+ private suspend fun showExitWindowingTooltip(tooltipColorScheme: TooltipColorScheme) {
windowDecorCaptionHandleRepository.captionStateFlow
// After the previous tooltip was dismissed, wait for 400 ms and see if the user entered
// desktop mode.
@@ -238,6 +251,7 @@
val exitWindowingTooltipConfig =
EducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
+ tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
tooltipText = getString(R.string.windowing_desktop_mode_exit_education_tooltip),
arrowDirection =
@@ -254,6 +268,32 @@
}
}
+ private fun tooltipColorScheme(captionState: CaptionState): TooltipColorScheme {
+ context.withStyledAttributes(
+ set = null,
+ attrs =
+ intArrayOf(
+ com.android.internal.R.attr.materialColorOnTertiaryFixed,
+ com.android.internal.R.attr.materialColorTertiaryFixed,
+ com.android.internal.R.attr.materialColorTertiaryFixedDim),
+ defStyleAttr = 0,
+ defStyleRes = 0) {
+ val onTertiaryFixed = getColor(/* index= */ 0, /* defValue= */ 0)
+ val tertiaryFixed = getColor(/* index= */ 1, /* defValue= */ 0)
+ val tertiaryFixedDim = getColor(/* index= */ 2, /* defValue= */ 0)
+ val taskInfo = (captionState as CaptionState.AppHandle).runningTaskInfo
+
+ val tooltipContainerColor =
+ if (decorThemeUtil.getAppTheme(taskInfo) == Theme.LIGHT) {
+ tertiaryFixed
+ } else {
+ tertiaryFixedDim
+ }
+ return TooltipColorScheme(tooltipContainerColor, onTertiaryFixed, onTertiaryFixed)
+ }
+ return TooltipColorScheme(0, 0, 0)
+ }
+
/**
* Setup callbacks for app handle education tooltips.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index ffcc526..d6b920e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -27,6 +27,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.window.flags.Flags;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -36,6 +38,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
* The {@link Transitions.TransitionHandler} that handles freeform task launches, closes,
@@ -44,7 +47,9 @@
*/
public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
private final Transitions mTransitions;
+ private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler;
private final WindowDecorViewModel mWindowDecorViewModel;
+ private final Optional<TaskChangeListener> mTaskChangeListener;
private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo =
new HashMap<>();
@@ -53,9 +58,13 @@
Context context,
ShellInit shellInit,
Transitions transitions,
- WindowDecorViewModel windowDecorViewModel) {
+ Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler,
+ WindowDecorViewModel windowDecorViewModel,
+ Optional<TaskChangeListener> taskChangeListener) {
mTransitions = transitions;
+ mImmersiveTransitionHandler = immersiveTransitionHandler;
mWindowDecorViewModel = windowDecorViewModel;
+ mTaskChangeListener = taskChangeListener;
if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) {
shellInit.addInitCallback(this::onInit, this);
}
@@ -72,6 +81,13 @@
@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startT,
@NonNull SurfaceControl.Transaction finishT) {
+ if (Flags.enableFullyImmersiveInDesktop()) {
+ // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
+ // is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
+ // Otherwise window decoration relayout won't run with the immersive state up to date.
+ mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition));
+ }
+
final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>();
final ArrayList<WindowContainerToken> taskParents = new ArrayList<>();
for (TransitionInfo.Change change : info.getChanges()) {
@@ -120,29 +136,39 @@
TransitionInfo.Change change,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
+ mTaskChangeListener.ifPresent(
+ listener -> listener.onTaskOpening(change.getTaskInfo()));
mWindowDecorViewModel.onTaskOpening(
- change.getTaskInfo(), change.getLeash(), startT, finishT);
+ change.getTaskInfo(), change.getLeash(), startT, finishT);
}
private void onCloseTransitionReady(
TransitionInfo.Change change,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
+ mTaskChangeListener.ifPresent(
+ listener -> listener.onTaskClosing(change.getTaskInfo()));
mWindowDecorViewModel.onTaskClosing(change.getTaskInfo(), startT, finishT);
+
}
private void onChangeTransitionReady(
TransitionInfo.Change change,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
+ mTaskChangeListener.ifPresent(listener ->
+ listener.onTaskChanging(change.getTaskInfo()));
mWindowDecorViewModel.onTaskChanging(
change.getTaskInfo(), change.getLeash(), startT, finishT);
}
+
private void onToFrontTransitionReady(
TransitionInfo.Change change,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
+ mTaskChangeListener.ifPresent(
+ listener -> listener.onTaskMovingToFront(change.getTaskInfo()));
mWindowDecorViewModel.onTaskChanging(
change.getTaskInfo(), change.getLeash(), startT, finishT);
}
@@ -179,4 +205,4 @@
mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i));
}
}
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt
new file mode 100644
index 0000000..f07c069
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.wm.shell.freeform
+
+import android.app.ActivityManager.RunningTaskInfo;
+
+/**
+ * Interface used by [FreeformTaskTransitionObserver] to manage freeform tasks.
+ *
+ * The implementations are responsible for handle all the task management.
+ */
+interface TaskChangeListener {
+ /** Notifies a task opening in freeform mode. */
+ fun onTaskOpening(taskInfo: RunningTaskInfo)
+
+ /** Notifies a task info update on the given task. */
+ fun onTaskChanging(taskInfo: RunningTaskInfo)
+
+ /** Notifies a task moving to the front. */
+ fun onTaskMovingToFront(taskInfo: RunningTaskInfo)
+
+ /** Notifies a task moving to the back. */
+ fun onTaskMovingToBack(taskInfo: RunningTaskInfo)
+
+ /** Notifies a task is closing. */
+ fun onTaskClosing(taskInfo: RunningTaskInfo)
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index bf175b7..bcf48d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -538,6 +538,14 @@
decoration.closeMaximizeMenu();
}
+ private void onEnterOrExitImmersive(int taskId) {
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+ if (decoration == null) {
+ return;
+ }
+ mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo);
+ }
+
private void onSnapResize(int taskId, boolean left) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
if (decoration == null) {
@@ -755,7 +763,16 @@
// back to the decoration using
// {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which
// should shared with the maximize menu's maximize/restore actions.
- onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+ if (Flags.enableFullyImmersiveInDesktop()
+ && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) {
+ // Task is requesting immersive, so it should either enter or exit immersive,
+ // depending on immersive state.
+ onEnterOrExitImmersive(decoration.mTaskInfo.taskId);
+ } else {
+ // Full immersive is disabled or task doesn't request/support it, so just
+ // toggle between maximize/restore states.
+ onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+ }
} else if (id == R.id.minimize_window) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
@@ -935,14 +952,18 @@
}
final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window
|| id == R.id.open_menu_button || id == R.id.minimize_window);
+ final boolean dragAllowed =
+ !mDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId);
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
- mDragPointerId = e.getPointerId(0);
- final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
- 0 /* ctrlType */, e.getRawX(0),
- e.getRawY(0));
- updateDragStatus(e.getActionMasked());
- mOnDragStartInitialBounds.set(initialBounds);
+ if (dragAllowed) {
+ mDragPointerId = e.getPointerId(0);
+ final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
+ 0 /* ctrlType */, e.getRawX(0),
+ e.getRawY(0));
+ updateDragStatus(e.getActionMasked());
+ mOnDragStartInitialBounds.set(initialBounds);
+ }
mHasLongClicked = false;
// Do not consume input event if a button is touched, otherwise it would
// prevent the button's ripple effect from showing.
@@ -951,6 +972,9 @@
case ACTION_MOVE: {
// If a decor's resize drag zone is active, don't also try to reposition it.
if (decoration.isHandlingDragResize()) break;
+ // Dragging the header isn't allowed, so skip the positioning work.
+ if (!dragAllowed) break;
+
decoration.closeMaximizeMenu();
if (e.findPointerIndex(mDragPointerId) == -1) {
mDragPointerId = e.getPointerId(0);
@@ -1036,6 +1060,10 @@
&& action != MotionEvent.ACTION_CANCEL)) {
return false;
}
+ if (mDesktopRepository.isTaskInFullImmersiveState(mTaskId)) {
+ // Disallow double-tap to resize when in full immersive.
+ return false;
+ }
onMaximizeOrRestore(mTaskId, "double_tap");
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index c7e8422..25d37fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -517,8 +517,8 @@
closeManageWindowsMenu();
closeMaximizeMenu();
}
- updateDragResizeListener(oldDecorationSurface);
- updateMaximizeMenu(startT);
+ updateDragResizeListener(oldDecorationSurface, inFullImmersive);
+ updateMaximizeMenu(startT, inFullImmersive);
Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
}
@@ -571,11 +571,12 @@
return mUserContext.getUser();
}
- private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
- if (!isDragResizable(mTaskInfo)) {
+ private void updateDragResizeListener(SurfaceControl oldDecorationSurface,
+ boolean inFullImmersive) {
+ if (!isDragResizable(mTaskInfo, inFullImmersive)) {
if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
// We still want to track caption bar's exclusion region on a non-resizeable task.
- updateExclusionRegion();
+ updateExclusionRegion(inFullImmersive);
}
closeDragResizeListener();
return;
@@ -609,11 +610,16 @@
getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
- updateExclusionRegion();
+ updateExclusionRegion(inFullImmersive);
}
}
- private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo,
+ boolean inFullImmersive) {
+ if (inFullImmersive) {
+ // Task cannot be resized in full immersive.
+ return false;
+ }
if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
return taskInfo.isFreeform();
}
@@ -677,8 +683,8 @@
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
}
- private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
- if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+ private void updateMaximizeMenu(SurfaceControl.Transaction startT, boolean inFullImmersive) {
+ if (!isDragResizable(mTaskInfo, inFullImmersive) || !isMaximizeMenuActive()) {
return;
}
if (!mTaskInfo.isVisible()) {
@@ -1546,24 +1552,29 @@
mPositionInParent.set(mTaskInfo.positionInParent);
}
- private void updateExclusionRegion() {
+ private void updateExclusionRegion(boolean inFullImmersive) {
// An outdated position in parent is one reason for this to be called; update it here.
updatePositionInParent();
mExclusionRegionListener
- .onExclusionRegionChanged(mTaskInfo.taskId, getGlobalExclusionRegion());
+ .onExclusionRegionChanged(mTaskInfo.taskId,
+ getGlobalExclusionRegion(inFullImmersive));
}
/**
* Create a new exclusion region from the corner rects (if resizeable) and caption bounds
* of this task.
*/
- private Region getGlobalExclusionRegion() {
+ private Region getGlobalExclusionRegion(boolean inFullImmersive) {
Region exclusionRegion;
- if (mDragResizeListener != null && isDragResizable(mTaskInfo)) {
+ if (mDragResizeListener != null && isDragResizable(mTaskInfo, inFullImmersive)) {
exclusionRegion = mDragResizeListener.getCornersRegion();
} else {
exclusionRegion = new Region();
}
+ if (inFullImmersive) {
+ // Task can't be moved in full immersive, so skip excluding the caption region.
+ return exclusionRegion;
+ }
exclusionRegion.union(new Rect(0, 0, mResult.mWidth,
getCaptionHeight(mTaskInfo.getWindowingMode())));
exclusionRegion.translate(mPositionInParent.x, mPositionInParent.y);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
index a9a16bc..c61b31e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor.education
+import android.annotation.ColorInt
import android.annotation.DimenRes
import android.annotation.LayoutRes
import android.content.Context
@@ -32,6 +33,7 @@
import android.widget.TextView
import android.window.DisplayAreaInfo
import android.window.WindowContainerTransaction
+import androidx.core.graphics.drawable.DrawableCompat
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.wm.shell.R
@@ -120,6 +122,7 @@
hideEducationTooltip()
tooltipViewConfig.onEducationClickAction()
}
+ setTooltipColorScheme(tooltipViewConfig.tooltipColorScheme)
}
val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection)
@@ -189,6 +192,21 @@
view = tooltipView)
}
+ private fun View.setTooltipColorScheme(tooltipColorScheme: TooltipColorScheme) {
+ requireViewById<LinearLayout>(R.id.tooltip_container).apply {
+ background.setTint(tooltipColorScheme.container)
+ }
+ requireViewById<ImageView>(R.id.arrow_icon).apply {
+ val wrappedDrawable = DrawableCompat.wrap(this.drawable)
+ DrawableCompat.setTint(wrappedDrawable, tooltipColorScheme.container)
+ }
+ requireViewById<TextView>(R.id.tooltip_text).apply { setTextColor(tooltipColorScheme.text) }
+ requireViewById<ImageView>(R.id.tooltip_icon).apply {
+ val wrappedDrawable = DrawableCompat.wrap(this.drawable)
+ DrawableCompat.setTint(wrappedDrawable, tooltipColorScheme.icon)
+ }
+ }
+
private fun tooltipViewGlobalCoordinates(
tooltipViewGlobalCoordinates: Point,
arrowDirection: TooltipArrowDirection,
@@ -255,6 +273,7 @@
*/
data class EducationViewConfig(
@LayoutRes val tooltipViewLayout: Int,
+ val tooltipColorScheme: TooltipColorScheme,
val tooltipViewGlobalCoordinates: Point,
val tooltipText: String,
val arrowDirection: TooltipArrowDirection,
@@ -262,6 +281,19 @@
val onDismissAction: () -> Unit,
)
+ /**
+ * Color scheme of education view:
+ *
+ * @property container Color of the container of the tooltip.
+ * @property text Text color of the [TextView] of education tooltip.
+ * @property icon Color to be filled in tooltip's icon.
+ */
+ data class TooltipColorScheme(
+ @ColorInt val container: Int,
+ @ColorInt val text: Int,
+ @ColorInt val icon: Int,
+ )
+
/** Direction of arrow of the tooltip */
enum class TooltipArrowDirection {
UP,
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
index 4165ed0..3c95d3a 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
@@ -122,6 +122,7 @@
instrumentation_target_package: "com.android.wm.shell.flicker.pip",
test_config_template: "AndroidTestTemplate.xml",
srcs: ["src/**/*.kt"],
+ exclude_srcs: [":WMShellFlickerTestsPipApps-src"],
static_libs: ["WMShellFlickerTestsBase"],
data: ["trace_config/*"],
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
new file mode 100644
index 0000000..cae6095
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [DesktopFullImmersiveTransitionHandler].
+ *
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+
+ @Mock private lateinit var mockTransitions: Transitions
+ private lateinit var desktopRepository: DesktopRepository
+ private val transactionSupplier = { SurfaceControl.Transaction() }
+
+ private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
+
+ @Before
+ fun setUp() {
+ desktopRepository = DesktopRepository(
+ context, ShellInit(TestShellExecutor()), mock(), mock()
+ )
+ immersiveHandler = DesktopFullImmersiveTransitionHandler(
+ transitions = mockTransitions,
+ desktopRepository = desktopRepository,
+ transactionSupplier = transactionSupplier
+ )
+ }
+
+ @Test
+ fun enterImmersive_transitionReady_updatesRepository() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.onTransitionReady(mockBinder)
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
+ }
+
+ @Test
+ fun exitImmersive_transitionReady_updatesRepository() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.onTransitionReady(mockBinder)
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+ }
+
+ @Test
+ fun enterImmersive_inProgress_ignores() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+
+ immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.enterImmersive(task, wct)
+
+ verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ }
+
+ @Test
+ fun exitImmersive_inProgress_ignores() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+
+ immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.exitImmersive(task, wct)
+
+ verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ }
+}
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 0ccd160..9e5c1a6 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
@@ -147,6 +147,7 @@
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
@@ -183,6 +184,8 @@
@Mock
lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
@Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
+ @Mock
+ lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler
@Mock lateinit var launchAdjacentController: LaunchAdjacentController
@Mock lateinit var splitScreenController: SplitScreenController
@Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@@ -289,6 +292,7 @@
dragAndDropTransitionHandler,
toggleResizeDesktopTaskTransitionHandler,
dragToDesktopTransitionHandler,
+ mockDesktopFullImmersiveTransitionHandler,
taskRepository,
desktopModeLoggerTransitionObserver,
launchAdjacentController,
@@ -3123,6 +3127,30 @@
verify(shellController, times(1)).addUserChangeListener(any())
}
+ @Test
+ fun toggleImmersive_enter_resizesToDisplayBounds() {
+ val task = setUpFreeformTask(DEFAULT_DISPLAY)
+ taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
+
+ controller.toggleDesktopTaskFullImmersiveState(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
+ wct.hasBoundsChange(task.token, Rect())
+ })
+ }
+
+ @Test
+ fun toggleImmersive_exit_resizesToStableBounds() {
+ val task = setUpFreeformTask(DEFAULT_DISPLAY)
+ taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
+
+ controller.toggleDesktopTaskFullImmersiveState(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
+ wct.hasBoundsChange(task.token, STABLE_BOUNDS)
+ })
+ }
+
/**
* Assert that an unhandled drag event launches a PendingIntent with the
* windowing mode and bounds we are expecting.
@@ -3488,6 +3516,13 @@
.isEqualTo(windowingMode)
}
+private fun WindowContainerTransaction.hasBoundsChange(
+ token: WindowContainerToken,
+ bounds: Rect
+): Boolean = this.changes.any { change ->
+ change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
+}
+
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 499e339..86a8502 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -17,8 +17,11 @@
package com.android.wm.shell.freeform;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -30,6 +33,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import android.window.IWindowContainerToken;
import android.window.TransitionInfo;
@@ -37,30 +42,43 @@
import androidx.test.filters.SmallTest;
+import com.android.window.flags.Flags;
+
+import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
+import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.TransitionInfoBuilder;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
+import java.util.Optional;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Optional;
+
/**
- * Tests of {@link FreeformTaskTransitionObserver}
+ * Tests for {@link FreeformTaskTransitionObserver}.
*/
@SmallTest
public class FreeformTaskTransitionObserverTest {
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock
private ShellInit mShellInit;
@Mock
private Transitions mTransitions;
@Mock
+ private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler;
+ @Mock
private WindowDecorViewModel mWindowDecorViewModel;
-
+ @Mock
+ private TaskChangeListener mTaskChangeListener;
private FreeformTaskTransitionObserver mTransitionObserver;
@Before
@@ -69,12 +87,14 @@
PackageManager pm = mock(PackageManager.class);
doReturn(true).when(pm).hasSystemFeature(
- PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT);
+ PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT);
final Context context = mock(Context.class);
doReturn(pm).when(context).getPackageManager();
mTransitionObserver = new FreeformTaskTransitionObserver(
- context, mShellInit, mTransitions, mWindowDecorViewModel);
+ context, mShellInit, mTransitions,
+ Optional.of(mDesktopFullImmersiveTransitionHandler),
+ mWindowDecorViewModel, Optional.of(mTaskChangeListener));
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
Runnable.class);
@@ -87,12 +107,12 @@
}
@Test
- public void testRegistersObserverAtInit() {
+ public void init_registersObserver() {
verify(mTransitions).registerObserver(same(mTransitionObserver));
}
@Test
- public void testCreatesWindowDecorOnOpenTransition_freeform() {
+ public void openTransition_createsWindowDecor() {
final TransitionInfo.Change change =
createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -109,7 +129,55 @@
}
@Test
- public void testPreparesWindowDecorOnCloseTransition_freeform() {
+ public void openTransition_notifiesOnTaskOpening() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(change).build();
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mTaskChangeListener).onTaskOpening(change.getTaskInfo());
+ }
+
+ @Test
+ public void toFrontTransition_notifiesOnTaskMovingToFront() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_TO_FRONT, /* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, /* flags= */ 0)
+ .addChange(change).build();
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mTaskChangeListener).onTaskMovingToFront(change.getTaskInfo());
+ }
+
+ @Test
+ public void changeTransition_notifiesOnTaskChanging() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CHANGE, /* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, /* flags= */ 0)
+ .addChange(change).build();
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mTaskChangeListener).onTaskChanging(change.getTaskInfo());
+ }
+
+ @Test
+ public void closeTransition_preparesWindowDecor() {
final TransitionInfo.Change change =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
@@ -126,7 +194,23 @@
}
@Test
- public void testDoesntCloseWindowDecorDuringCloseTransition() throws Exception {
+ public void closeTransition_notifiesOnTaskClosing() {
+ final TransitionInfo.Change change =
+ createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(change).build();
+
+ final IBinder transition = mock(IBinder.class);
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+ mTransitionObserver.onTransitionStarting(transition);
+
+ verify(mTaskChangeListener).onTaskClosing(change.getTaskInfo());
+ }
+
+ @Test
+ public void closeTransition_doesntCloseWindowDecorDuringTransition() throws Exception {
final TransitionInfo.Change change =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
@@ -142,7 +226,7 @@
}
@Test
- public void testClosesWindowDecorAfterCloseTransition() throws Exception {
+ public void closeTransition_closesWindowDecorAfterTransition() throws Exception {
final TransitionInfo.Change change =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
@@ -161,7 +245,7 @@
}
@Test
- public void testClosesMergedWindowDecorationAfterTransitionFinishes() throws Exception {
+ public void transitionFinished_closesMergedWindowDecoration() throws Exception {
// The playing transition
final TransitionInfo.Change change1 =
createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM);
@@ -192,7 +276,7 @@
}
@Test
- public void testClosesAllWindowDecorsOnTransitionMergeAfterCloseTransitions() throws Exception {
+ public void closeTransition_closesWindowDecorsOnTransitionMerge() throws Exception {
// The playing transition
final TransitionInfo.Change change1 =
createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM);
@@ -223,6 +307,19 @@
verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo());
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void onTransitionReady_forwardsToDesktopImmersiveHandler() {
+ final IBinder transition = mock(IBinder.class);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build();
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+
+ verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+ }
+
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 83bd15b..4aa7e18 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -1221,9 +1221,48 @@
assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testMaximizeButtonClick_requestingImmersive_togglesDesktopImmersiveState() {
+ val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+ as ArgumentCaptor<View.OnClickListener>
+ val decor = createOpenTaskDecoration(
+ windowingMode = WINDOWING_MODE_FREEFORM,
+ onCaptionButtonClickListener = onClickListenerCaptor,
+ requestingImmersive = true,
+ )
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.maximize_window)
+
+ onClickListenerCaptor.value.onClick(view)
+
+ verify(mockDesktopTasksController)
+ .toggleDesktopTaskFullImmersiveState(decor.mTaskInfo)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testMaximizeButtonClick_notRequestingImmersive_togglesDesktopTaskSize() {
+ val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+ as ArgumentCaptor<View.OnClickListener>
+ val decor = createOpenTaskDecoration(
+ windowingMode = WINDOWING_MODE_FREEFORM,
+ onCaptionButtonClickListener = onClickListenerCaptor,
+ requestingImmersive = false,
+ )
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.maximize_window)
+
+ onClickListenerCaptor.value.onClick(view)
+
+ verify(mockDesktopTasksController)
+ .toggleDesktopTaskSize(decor.mTaskInfo)
+ }
+
private fun createOpenTaskDecoration(
@WindowingMode windowingMode: Int,
taskSurface: SurfaceControl = SurfaceControl(),
+ requestingImmersive: Boolean = false,
onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
@@ -1243,7 +1282,10 @@
onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> =
forClass(View.OnTouchListener::class.java) as ArgumentCaptor<View.OnTouchListener>
): DesktopModeWindowDecoration {
- val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode))
+ val decor = setUpMockDecorationForTask(createTask(
+ windowingMode = windowingMode,
+ requestingImmersive = requestingImmersive
+ ))
onTaskOpening(decor.mTaskInfo, taskSurface)
verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture())
verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture())
@@ -1282,6 +1324,7 @@
activityType: Int = ACTIVITY_TYPE_STANDARD,
focused: Boolean = true,
activityInfo: ActivityInfo = ActivityInfo(),
+ requestingImmersive: Boolean = false
): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
@@ -1292,6 +1335,11 @@
topActivityInfo = activityInfo
isFocused = focused
isResizeable = true
+ requestedVisibleTypes = if (requestingImmersive) {
+ statusBars().inv()
+ } else {
+ statusBars()
+ }
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
index 6749776..741dfb8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
@@ -30,12 +30,15 @@
import android.view.WindowManager
import android.widget.TextView
import android.window.WindowContainerTransaction
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.test.filters.SmallTest
import com.android.wm.shell.R
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -240,14 +243,42 @@
/* fromRotation= */ ROTATION_90,
/* toRotation= */ ROTATION_180,
/* newDisplayAreaInfo= */ null,
- WindowContainerTransaction())
+ WindowContainerTransaction(),
+ )
verify(mockPopupWindow, times(1)).releaseView()
verify(mockDisplayController, atLeastOnce()).removeDisplayChangingController(any())
}
+ @Test
+ fun showEducationTooltip_setTooltipColorScheme_correctColorsAreSet() {
+ val tooltipColorScheme =
+ TooltipColorScheme(
+ container = Color.Red.toArgb(), text = Color.Blue.toArgb(), icon = Color.Green.toArgb())
+ val tooltipViewConfig = createTooltipConfig(tooltipColorScheme = tooltipColorScheme)
+
+ tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+ verify(mockViewContainerFactory, times(1))
+ .create(
+ windowManagerWrapper = any(),
+ taskId = anyInt(),
+ x = anyInt(),
+ y = anyInt(),
+ width = anyInt(),
+ height = anyInt(),
+ flags = anyInt(),
+ view = tooltipViewArgumentCaptor.capture())
+ val tooltipTextView =
+ tooltipViewArgumentCaptor.lastValue.findViewById<TextView>(R.id.tooltip_text)
+ assertThat(tooltipTextView.textColors.defaultColor).isEqualTo(Color.Blue.toArgb())
+ }
+
private fun createTooltipConfig(
@LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip,
+ tooltipColorScheme: TooltipColorScheme =
+ TooltipColorScheme(
+ container = Color.Red.toArgb(), text = Color.Red.toArgb(), icon = Color.Red.toArgb()),
tooltipViewGlobalCoordinates: Point = Point(0, 0),
tooltipText: String = "This is a tooltip",
arrowDirection: TooltipArrowDirection = TooltipArrowDirection.UP,
@@ -256,6 +287,7 @@
) =
DesktopWindowingEducationTooltipController.EducationViewConfig(
tooltipViewLayout = tooltipViewLayout,
+ tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates,
tooltipText = tooltipText,
arrowDirection = arrowDirection,
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index ede385a..9cd6e25 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -48,6 +48,7 @@
minikinPaint.localeListId = paint->getMinikinLocaleListId();
minikinPaint.fontStyle = resolvedFace->fStyle;
minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings();
+ minikinPaint.fontVariationSettings = paint->getFontVariationOverride();
const std::optional<minikin::FamilyVariant>& familyVariant = paint->getFamilyVariant();
if (familyVariant.has_value()) {
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index f3c5a18..456fedf 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -948,6 +948,9 @@
<!-- Permission required for CTS test - CtsNfcTestCases -->
<uses-permission android:name="android.permission.NFC_SET_CONTROLLER_ALWAYS_ON" />
+ <!-- Permission required for CTS test - CtsAppTestCases -->
+ <uses-permission android:name="android.permission.KILL_UID" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 1e9541e..6d1d9cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -189,6 +189,7 @@
internalTransitionInteractor.currentTransitionInfoInternal,
keyguardInteractor.statusBarState,
keyguardInteractor.isKeyguardDismissible,
+ keyguardInteractor.isKeyguardOccluded,
)
.collect {
(
@@ -196,7 +197,8 @@
startedStep,
currentTransitionInfo,
statusBarState,
- isKeyguardUnlocked) ->
+ isKeyguardUnlocked,
+ isKeyguardOccluded) ->
val id = transitionId
if (id != null) {
if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) {
@@ -236,9 +238,13 @@
if (nextState == TransitionState.CANCELED) {
transitionRepository.startTransition(
TransitionInfo(
- ownerName = name,
+ ownerName =
+ "$name " +
+ "(on behalf of FromPrimaryBouncerInteractor)",
from = KeyguardState.PRIMARY_BOUNCER,
- to = KeyguardState.LOCKSCREEN,
+ to =
+ if (isKeyguardOccluded) KeyguardState.OCCLUDED
+ else KeyguardState.LOCKSCREEN,
modeOnCanceled = TransitionModeOnCanceled.REVERSE,
animator =
getDefaultAnimatorForTransitionsToState(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 5896659..2bff7c86 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -30,6 +30,7 @@
import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.AOD;
import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING;
import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED;
import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
@@ -1213,6 +1214,16 @@
}, mMainDispatcher);
}
+ if (MigrateClocksToBlueprint.isEnabled()) {
+ collectFlow(mView, mKeyguardTransitionInteractor.transition(
+ Edge.Companion.create(AOD, LOCKSCREEN)),
+ (TransitionStep step) -> {
+ if (step.getTransitionState() == TransitionState.FINISHED) {
+ updateExpandedHeightToMaxHeight();
+ }
+ }, mMainDispatcher);
+ }
+
// Ensures that flags are updated when an activity launches
collectFlow(mView,
mShadeAnimationInteractor.isLaunchingActivity(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
index 8c8f200..695e088 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
@@ -18,6 +18,8 @@
import android.content.Context
import android.icu.text.MessageFormat
+import com.android.app.tracing.coroutines.flow.flowOn
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.res.R
@@ -32,6 +34,7 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.Locale
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -50,11 +53,16 @@
zenModeInteractor: ZenModeInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
notificationSettingsInteractor: NotificationSettingsInteractor,
+ @Background bgDispatcher: CoroutineDispatcher,
dumpManager: DumpManager,
) : FlowDumperImpl(dumpManager) {
val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(false)
+ } else if (ModesEmptyShadeFix.isEnabled) {
+ zenModeInteractor.areNotificationsHiddenInShade
+ .dumpWhileCollecting("areNotificationsHiddenInShade")
+ .flowOn(bgDispatcher)
} else {
zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
"areNotificationsHiddenInShade"
@@ -80,31 +88,33 @@
// recommended architecture, and making it so it reacts to changes for the new Modes.
// The former does not depend on the modes flags being on, but the latter does.
if (ModesUi.isEnabled) {
- zenModeInteractor.modesHidingNotifications.map { modes ->
- // Create a string that is either "No notifications" if no modes are filtering
- // them out, or something like "Notifications paused by SomeMode" otherwise.
- val msgFormat =
- MessageFormat(
- context.getString(R.string.modes_suppressing_shade_text),
- Locale.getDefault(),
- )
- val count = modes.count()
- val args: MutableMap<String, Any> = HashMap()
- args["count"] = count
- if (count >= 1) {
- args["mode"] = modes[0].name
+ zenModeInteractor.modesHidingNotifications.map { modes ->
+ // Create a string that is either "No notifications" if no modes are
+ // filtering
+ // them out, or something like "Notifications paused by SomeMode" otherwise.
+ val msgFormat =
+ MessageFormat(
+ context.getString(R.string.modes_suppressing_shade_text),
+ Locale.getDefault(),
+ )
+ val count = modes.count()
+ val args: MutableMap<String, Any> = HashMap()
+ args["count"] = count
+ if (count >= 1) {
+ args["mode"] = modes[0].name
+ }
+ msgFormat.format(args)
}
- msgFormat.format(args)
- }
- } else {
- areNotificationsHiddenInShade.map { areNotificationsHiddenInShade ->
- if (areNotificationsHiddenInShade) {
- context.getString(R.string.dnd_suppressing_shade_text)
- } else {
- context.getString(R.string.empty_shade_text)
+ } else {
+ areNotificationsHiddenInShade.map { areNotificationsHiddenInShade ->
+ if (areNotificationsHiddenInShade) {
+ context.getString(R.string.dnd_suppressing_shade_text)
+ } else {
+ context.getString(R.string.empty_shade_text)
+ }
}
}
- }
+ .flowOn(bgDispatcher)
}
}
@@ -120,23 +130,24 @@
val onClick: Flow<SettingsIntent> by lazy {
ModesEmptyShadeFix.assertInNewMode()
combine(
- zenModeInteractor.modesHidingNotifications,
- notificationSettingsInteractor.isNotificationHistoryEnabled,
- ) { modes, isNotificationHistoryEnabled ->
- if (modes.isNotEmpty()) {
- if (modes.size == 1) {
- SettingsIntent.forModeSettings(modes[0].id)
+ zenModeInteractor.modesHidingNotifications,
+ notificationSettingsInteractor.isNotificationHistoryEnabled,
+ ) { modes, isNotificationHistoryEnabled ->
+ if (modes.isNotEmpty()) {
+ if (modes.size == 1) {
+ SettingsIntent.forModeSettings(modes[0].id)
+ } else {
+ SettingsIntent.forModesSettings()
+ }
} else {
- SettingsIntent.forModesSettings()
- }
- } else {
- if (isNotificationHistoryEnabled) {
- SettingsIntent.forNotificationHistory()
- } else {
- SettingsIntent.forNotificationSettings()
+ if (isNotificationHistoryEnabled) {
+ SettingsIntent.forNotificationHistory()
+ } else {
+ SettingsIntent.forNotificationSettings()
+ }
}
}
- }
+ .flowOn(bgDispatcher)
}
@AssistedFactory
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index f5a90196..0e9ef06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -556,11 +556,13 @@
return null;
}).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class));
- // Dreaming->Lockscreen
+ // Any edge transition
when(mKeyguardTransitionInteractor.transition(any()))
.thenReturn(emptyFlow());
when(mKeyguardTransitionInteractor.transition(any(), any()))
.thenReturn(emptyFlow());
+
+ // Dreaming->Lockscreen
when(mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha())
.thenReturn(emptyFlow());
when(mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(anyInt()))
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
index 8fdb948..ca33a86 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
@@ -19,6 +19,7 @@
import android.content.applicationContext
import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -30,6 +31,7 @@
zenModeInteractor,
seenNotificationsInteractor,
notificationSettingsInteractor,
+ testDispatcher,
dumpManager,
)
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index c37d471..1563a62 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -286,6 +286,7 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -4030,7 +4031,6 @@
&& isFullVolumeDevice(device);
boolean tvConditions = mHdmiTvClient != null
&& mHdmiSystemAudioSupported
- && isFullVolumeDevice(device)
&& !isAbsoluteVolumeDevice(device)
&& !isA2dpAbsoluteVolumeDevice(device);
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
index bc58501..93b0e66 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java
@@ -123,7 +123,8 @@
enrollSuccessful,
-1, /* sensorId */
ambientLightLux,
- source);
+ source,
+ -1 /* templateId*/);
}
/** {@see FrameworkStatsLog.BIOMETRIC_ERROR_OCCURRED}. */
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java
index 46a8f03..1c947e9 100644
--- a/services/core/java/com/android/server/hdmi/HdmiEarcController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java
@@ -87,8 +87,8 @@
} catch (ServiceSpecificException sse) {
HdmiLogger.error(
"Could not set eARC enabled to " + enabled + ". Error: ", sse.errorCode);
- } catch (RemoteException re) {
- HdmiLogger.error("Could not set eARC enabled to " + enabled + ":. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not set eARC enabled to " + enabled + ":. Exception: ", e);
}
}
@@ -96,8 +96,8 @@
public boolean nativeIsEarcEnabled() {
try {
return mEarc.isEArcEnabled();
- } catch (RemoteException re) {
- HdmiLogger.error("Could not read if eARC is enabled. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not read if eARC is enabled. Exception: ", e);
return false;
}
}
@@ -107,8 +107,8 @@
mEarcCallback = callback;
try {
mEarc.setCallback(callback);
- } catch (RemoteException re) {
- HdmiLogger.error("Could not set callback. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not set callback. Exception: ", e);
}
}
@@ -116,8 +116,8 @@
public byte nativeGetState(int portId) {
try {
return mEarc.getState(portId);
- } catch (RemoteException re) {
- HdmiLogger.error("Could not get eARC state. Exception: ", re);
+ } catch (RemoteException | NullPointerException e) {
+ HdmiLogger.error("Could not get eARC state. Exception: ", e);
return -1;
}
}
@@ -126,9 +126,9 @@
public byte[] nativeGetLastReportedAudioCapabilities(int portId) {
try {
return mEarc.getLastReportedAudioCapabilities(portId);
- } catch (RemoteException re) {
+ } catch (RemoteException | NullPointerException e) {
HdmiLogger.error(
- "Could not read last reported audio capabilities. Exception: ", re);
+ "Could not read last reported audio capabilities. Exception: ", e);
return null;
}
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index b1b1637..34d939b 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -126,6 +126,7 @@
import com.android.server.SystemServiceManager;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.utils.RequestThrottle;
+import com.android.server.pm.verify.pkg.VerifierController;
import libcore.io.IoUtils;
@@ -213,6 +214,7 @@
private final StagingManager mStagingManager;
private AppOpsManager mAppOps;
+ private final VerifierController mVerifierController;
private final HandlerThread mInstallThread;
private final Handler mInstallHandler;
@@ -325,6 +327,7 @@
mGentleUpdateHelper = new GentleUpdateHelper(
context, mInstallThread.getLooper(), new AppStateHelper(context));
mPackageArchiver = new PackageArchiver(mContext, mPm);
+ mVerifierController = new VerifierController(mContext, mInstallHandler);
LocalServices.getService(SystemServiceManager.class).startService(
new Lifecycle(context, this));
@@ -521,7 +524,8 @@
try {
session = PackageInstallerSession.readFromXml(in, mInternalCallback,
mContext, mPm, mInstallThread.getLooper(), mStagingManager,
- mSessionsDir, this, mSilentUpdatePolicy);
+ mSessionsDir, this, mSilentUpdatePolicy,
+ mVerifierController);
} catch (Exception e) {
Slog.e(TAG, "Could not read session", e);
continue;
@@ -1037,7 +1041,8 @@
mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
- false, false, false, PackageManager.INSTALL_UNKNOWN, "", null);
+ false, false, false, PackageManager.INSTALL_UNKNOWN, "", null,
+ mVerifierController);
synchronized (mSessions) {
mSessions.put(sessionId, session);
@@ -1047,6 +1052,7 @@
mCallbacks.notifySessionCreated(session.sessionId, session.userId);
mSettingsWriteRequest.schedule();
+
if (LOGD) {
Slog.d(TAG, "Created session id=" + sessionId + " staged=" + params.isStaged);
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index ff8a69d..c581622 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -38,6 +38,7 @@
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_STAGED;
import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
+import static android.content.pm.verify.pkg.VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN;
import static android.os.Process.INVALID_UID;
import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
import static android.system.OsConstants.O_CREAT;
@@ -87,6 +88,7 @@
import android.content.pm.DataLoaderParams;
import android.content.pm.DataLoaderParamsParcel;
import android.content.pm.FileSystemControlParcel;
+import android.content.pm.Flags;
import android.content.pm.IDataLoader;
import android.content.pm.IDataLoaderStatusListener;
import android.content.pm.IOnChecksumsReadyListener;
@@ -108,6 +110,7 @@
import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
+import android.content.pm.SigningInfo;
import android.content.pm.dex.DexMetadataHelper;
import android.content.pm.parsing.ApkLite;
import android.content.pm.parsing.ApkLiteParseUtils;
@@ -115,6 +118,7 @@
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.pm.verify.domain.DomainSet;
+import android.content.pm.verify.pkg.VerificationStatus;
import android.content.res.ApkAssets;
import android.content.res.AssetManager;
import android.content.res.Configuration;
@@ -122,6 +126,7 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.icu.util.ULocale;
+import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -133,6 +138,7 @@
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelableException;
+import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.RevocableFileDescriptor;
@@ -190,6 +196,7 @@
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.verify.pkg.VerifierController;
import libcore.io.IoUtils;
import libcore.util.EmptyArray;
@@ -218,6 +225,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
+import java.util.function.Supplier;
public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private static final String TAG = "PackageInstallerSession";
@@ -404,6 +412,7 @@
* Note all calls must be done outside {@link #mLock} to prevent lock inversion.
*/
private final StagingManager mStagingManager;
+ @NonNull private final VerifierController mVerifierController;
final int sessionId;
final int userId;
@@ -1156,7 +1165,8 @@
boolean prepared, boolean committed, boolean destroyed, boolean sealed,
@Nullable int[] childSessionIds, int parentSessionId, boolean isReady,
boolean isFailed, boolean isApplied, int sessionErrorCode,
- String sessionErrorMessage, DomainSet preVerifiedDomains) {
+ String sessionErrorMessage, DomainSet preVerifiedDomains,
+ @NonNull VerifierController verifierController) {
mCallback = callback;
mContext = context;
mPm = pm;
@@ -1165,6 +1175,7 @@
mSilentUpdatePolicy = silentUpdatePolicy;
mHandler = new Handler(looper, mHandlerCallback);
mStagingManager = stagingManager;
+ mVerifierController = verifierController;
this.sessionId = sessionId;
this.userId = userId;
@@ -1249,6 +1260,14 @@
"Archived installation can only use Streaming System DataLoader.");
}
}
+
+ if (Flags.verificationService()) {
+ // Start binding to the verification service, if not bound already.
+ mVerifierController.bindToVerifierServiceIfNeeded(() -> pm.snapshotComputer(), userId);
+ if (!TextUtils.isEmpty(params.appPackageName)) {
+ mVerifierController.notifyPackageNameAvailable(params.appPackageName);
+ }
+ }
}
PackageInstallerHistoricalSession createHistoricalSession() {
@@ -2821,7 +2840,35 @@
// since installation is in progress.
activate();
}
+ if (Flags.verificationService()) {
+ final Supplier<Computer> snapshotSupplier = mPm::snapshotComputer;
+ if (mVerifierController.isVerifierInstalled(snapshotSupplier, userId)) {
+ // TODO: extract shared library declarations
+ final SigningInfo signingInfo;
+ synchronized (mLock) {
+ signingInfo = new SigningInfo(mSigningDetails);
+ }
+ // Send the request to the verifier and wait for its response before the rest of
+ // the installation can proceed.
+ if (!mVerifierController.startVerificationSession(snapshotSupplier, userId,
+ sessionId, params.appPackageName, Uri.fromFile(stageDir), signingInfo,
+ /* declaredLibraries= */null, /* extensionParams= */ null,
+ new VerifierCallback(), /* retry= */ false)) {
+ // A verifier is installed but cannot be connected. Installation disallowed.
+ onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
+ "A verifier agent is available on device but cannot be connected.");
+ }
+ } else {
+ // Verifier is not installed. Let the installation pass for now.
+ resumeVerify();
+ }
+ } else {
+ // New verification feature is not enabled. Proceed to the rest of the verification.
+ resumeVerify();
+ }
+ }
+ private void resumeVerify() {
if (mVerificationInProgress) {
Slog.w(TAG, "Verification is already in progress for session " + sessionId);
return;
@@ -2856,6 +2903,66 @@
}
}
+ /**
+ * Used for the VerifierController to report status back.
+ */
+ public class VerifierCallback {
+ /**
+ * Called by the VerifierController when the connection has failed.
+ */
+ public void onConnectionFailed() {
+ mHandler.post(() -> {
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "A verifier agent is available on device but cannot be connected.");
+ });
+ }
+ /**
+ * Called by the VerifierController when the verification request has timed out.
+ */
+ public void onTimeout() {
+ mHandler.post(() -> {
+ mVerifierController.notifyVerificationTimeout(sessionId);
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "Verification timed out; missing a response from the verifier within the"
+ + " time limit");
+ });
+ }
+ /**
+ * Called by the VerifierController when the verification request has received a complete
+ * response.
+ */
+ public void onVerificationCompleteReceived(@NonNull VerificationStatus statusReceived,
+ @Nullable PersistableBundle extensionResponse) {
+ // TODO: handle extension response
+ mHandler.post(() -> {
+ if (statusReceived.isVerified()) {
+ // Continue with the rest of the verification and installation.
+ resumeVerify();
+ } else {
+ StringBuilder sb = new StringBuilder("Verifier rejected the installation");
+ if (!TextUtils.isEmpty(statusReceived.getFailureMessage())) {
+ sb.append(" with message: ").append(statusReceived.getFailureMessage());
+ }
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ sb.toString());
+ }
+ });
+ }
+ /**
+ * Called by the VerifierController when the verification request has received an incomplete
+ * response.
+ */
+ public void onVerificationIncompleteReceived(int incompleteReason) {
+ mHandler.post(() -> {
+ if (incompleteReason == VERIFICATION_INCOMPLETE_UNKNOWN) {
+ // TODO: change this to a user confirmation and handle other incomplete reasons
+ onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
+ "Verification cannot be completed for unknown reasons.");
+ }
+ });
+ }
+ }
+
private IntentSender getRemoteStatusReceiver() {
synchronized (mLock) {
return mRemoteStatusReceiver;
@@ -5369,6 +5476,14 @@
}
} catch (InstallerException ignored) {
}
+ if (Flags.verificationService()
+ && !TextUtils.isEmpty(params.appPackageName)
+ && !isCommitted()) {
+ // Only notify for the cancellation if the verification request has not
+ // been sent out, which happens right after commit() is called.
+ mVerifierController.notifyVerificationCancelled(
+ params.appPackageName);
+ }
}
void dump(IndentingPrintWriter pw) {
@@ -5768,7 +5883,8 @@
@NonNull PackageManagerService pm, Looper installerThread,
@NonNull StagingManager stagingManager, @NonNull File sessionsDir,
@NonNull PackageSessionProvider sessionProvider,
- @NonNull SilentUpdatePolicy silentUpdatePolicy)
+ @NonNull SilentUpdatePolicy silentUpdatePolicy,
+ @NonNull VerifierController verifierController)
throws IOException, XmlPullParserException {
final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID);
final int userId = in.getAttributeInt(null, ATTR_USER_ID);
@@ -5972,6 +6088,6 @@
installerUid, installSource, params, createdMillis, committedMillis, stageDir,
stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
- sessionErrorCode, sessionErrorMessage, preVerifiedDomains);
+ sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController);
}
}
diff --git a/services/core/java/com/android/server/pm/verify/pkg/VerificationStatusTracker.java b/services/core/java/com/android/server/pm/verify/pkg/VerificationStatusTracker.java
new file mode 100644
index 0000000..db747f9
--- /dev/null
+++ b/services/core/java/com/android/server/pm/verify/pkg/VerificationStatusTracker.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.verify.pkg;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * This class keeps record of the current timeout status of a verification request.
+ */
+public final class VerificationStatusTracker {
+ private final @CurrentTimeMillisLong long mStartTime;
+ private @CurrentTimeMillisLong long mTimeoutTime;
+ private final @CurrentTimeMillisLong long mMaxTimeoutTime;
+ @NonNull
+ private final VerifierController.Injector mInjector;
+ // Record the package name associated with the verification result
+ @NonNull
+ private final String mPackageName;
+
+ /**
+ * By default, the timeout time is the default timeout duration plus the current time (when
+ * the timer starts for a verification request). Both the default timeout time and the max
+ * timeout time cannot be changed after the timer has started, but the actual timeout time
+ * can be extended via {@link #extendTimeRemaining} to the maximum allowed.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public VerificationStatusTracker(@NonNull String packageName,
+ long defaultTimeoutMillis, long maxExtendedTimeoutMillis,
+ @NonNull VerifierController.Injector injector) {
+ mPackageName = packageName;
+ mStartTime = injector.getCurrentTimeMillis();
+ mTimeoutTime = mStartTime + defaultTimeoutMillis;
+ mMaxTimeoutTime = mStartTime + maxExtendedTimeoutMillis;
+ mInjector = injector;
+ }
+
+ /**
+ * Used by the controller to inform the verifier agent about the timestamp when the verification
+ * request will timeout.
+ */
+ public @CurrentTimeMillisLong long getTimeoutTime() {
+ return mTimeoutTime;
+ }
+
+ /**
+ * Used by the controller to decide when to check for timeout again.
+ * @return 0 if the timeout time has been reached, otherwise the remaining time in milliseconds
+ * before the timeout is reached.
+ */
+ public @CurrentTimeMillisLong long getRemainingTime() {
+ final long remainingTime = mTimeoutTime - mInjector.getCurrentTimeMillis();
+ if (remainingTime < 0) {
+ return 0;
+ }
+ return remainingTime;
+ }
+
+ /**
+ * Used by the controller to extend the timeout duration of the verification request, upon
+ * receiving the callback from the verifier agent.
+ * @return the amount of time in millis that the timeout has been extended, subject to the max
+ * amount allowed.
+ */
+ public long extendTimeRemaining(@CurrentTimeMillisLong long additionalMs) {
+ if (mTimeoutTime + additionalMs > mMaxTimeoutTime) {
+ additionalMs = mMaxTimeoutTime - mTimeoutTime;
+ }
+ mTimeoutTime += additionalMs;
+ return additionalMs;
+ }
+
+ /**
+ * Used by the controller to get the timeout status of the request.
+ * @return False if the request still has some time left before timeout, otherwise return True.
+ */
+ public boolean isTimeout() {
+ return mInjector.getCurrentTimeMillis() >= mTimeoutTime;
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+}
diff --git a/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
new file mode 100644
index 0000000..7eac940
--- /dev/null
+++ b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.verify.pkg;
+
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static android.os.Process.SYSTEM_UID;
+import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.verify.pkg.IVerificationSessionCallback;
+import android.content.pm.verify.pkg.IVerificationSessionInterface;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.server.pm.Computer;
+import com.android.server.pm.PackageInstallerSession;
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+/**
+ * This class manages the bind to the verifier agent installed on the device that implements
+ * {@link android.content.pm.verify.pkg.VerifierService} and handles all its interactions.
+ */
+public class VerifierController {
+ private static final String TAG = "VerifierController";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Configurable maximum amount of time in milliseconds to wait for a verifier to respond to
+ * a verification request.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS =
+ "verification_request_timeout_millis";
+ // Default duration to wait for a verifier to respond to a verification request.
+ private static final long DEFAULT_VERIFICATION_REQUEST_TIMEOUT_MILLIS =
+ TimeUnit.MINUTES.toMillis(1);
+ /**
+ * Configurable maximum amount of time in milliseconds that the verifier can request to extend
+ * the verification request timeout duration to. This is the maximum amount of time the system
+ * can wait for a request before it times out.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS =
+ "max_verification_request_extended_timeout_millis";
+ // Max duration allowed to wait for a verifier to respond to a verification request.
+ private static final long DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS =
+ TimeUnit.MINUTES.toMillis(10);
+ // The maximum amount of time to wait from the moment when the session requires a verification,
+ // till when the request is delivered to the verifier, pending the connection to be established.
+ private static final long CONNECTION_TIMEOUT_SECONDS = 10;
+ // The maximum amount of time to wait before the system unbinds from the verifier.
+ private static final long UNBIND_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(6);
+
+ private final Context mContext;
+ private final Handler mHandler;
+ @Nullable
+ private ServiceConnector<IVerifierService> mRemoteService;
+ @Nullable
+ private ComponentName mRemoteServiceComponentName;
+ @NonNull
+ private Injector mInjector;
+
+ // Repository of active verification sessions and their status, mapping from id to status.
+ @NonNull
+ @GuardedBy("mVerificationStatus")
+ private final SparseArray<VerificationStatusTracker> mVerificationStatus = new SparseArray<>();
+
+ public VerifierController(@NonNull Context context, @NonNull Handler handler) {
+ this(context, handler, new Injector());
+ }
+
+ @VisibleForTesting
+ public VerifierController(@NonNull Context context, @NonNull Handler handler,
+ @NonNull Injector injector) {
+ mContext = context;
+ mHandler = handler;
+ mInjector = injector;
+ }
+
+ /**
+ * Used by the installation session to check if a verifier is installed.
+ */
+ public boolean isVerifierInstalled(Supplier<Computer> snapshotSupplier, int userId) {
+ if (isVerifierConnected()) {
+ // Verifier is connected or is being connected, so it must be installed.
+ return true;
+ }
+ // Verifier has been disconnected, or it hasn't been connected. Check if it's installed.
+ return mInjector.isVerifierInstalled(snapshotSupplier.get(), userId);
+ }
+
+ /**
+ * Called to start querying and binding to a qualified verifier agent.
+ *
+ * @return False if a qualified verifier agent doesn't exist on device, so that the system can
+ * handle this situation immediately after the call.
+ * <p>
+ * Notice that since this is an async call, even if this method returns true, it doesn't
+ * necessarily mean that the binding connection was successful. However, the system will only
+ * try to bind once per installation session, so that it doesn't waste resource by repeatedly
+ * trying to bind if the verifier agent isn't available during a short amount of time.
+ * <p>
+ * If the verifier agent exists but cannot be started for some reason, all the notify* methods
+ * in this class will fail asynchronously and quietly. The system will learn about the failure
+ * after receiving the failure from
+ * {@link PackageInstallerSession.VerifierCallback#onConnectionFailed}.
+ */
+ public boolean bindToVerifierServiceIfNeeded(Supplier<Computer> snapshotSupplier, int userId) {
+ if (DEBUG) {
+ Slog.i(TAG, "Requesting to bind to the verifier service.");
+ }
+ if (mRemoteService != null) {
+ // Already connected
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier service is already connected.");
+ }
+ return true;
+ }
+ Pair<ServiceConnector<IVerifierService>, ComponentName> result =
+ mInjector.getRemoteService(snapshotSupplier.get(), mContext, userId, mHandler);
+ if (result == null || result.first == null) {
+ if (DEBUG) {
+ Slog.i(TAG, "Unable to find a qualified verifier.");
+ }
+ return false;
+ }
+ mRemoteService = result.first;
+ mRemoteServiceComponentName = result.second;
+ if (DEBUG) {
+ Slog.i(TAG, "Connecting to a qualified verifier: " + mRemoteServiceComponentName);
+ }
+ mRemoteService.setServiceLifecycleCallbacks(
+ new ServiceConnector.ServiceLifecycleCallbacks<>() {
+ @Override
+ public void onConnected(@NonNull IVerifierService service) {
+ Slog.i(TAG, "Verifier " + mRemoteServiceComponentName + " is connected");
+ }
+
+ @Override
+ public void onDisconnected(@NonNull IVerifierService service) {
+ Slog.w(TAG,
+ "Verifier " + mRemoteServiceComponentName + " is disconnected");
+ destroy();
+ }
+
+ @Override
+ public void onBinderDied() {
+ Slog.w(TAG, "Verifier " + mRemoteServiceComponentName + " has died");
+ destroy();
+ }
+
+ private void destroy() {
+ if (isVerifierConnected()) {
+ mRemoteService.unbind();
+ mRemoteService = null;
+ mRemoteServiceComponentName = null;
+ }
+ }
+ });
+ AndroidFuture<IVerifierService> unusedFuture = mRemoteService.connect();
+ return true;
+ }
+
+ private boolean isVerifierConnected() {
+ return mRemoteService != null && mRemoteServiceComponentName != null;
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a package name is available and will soon be
+ * requested for verification.
+ */
+ public void notifyPackageNameAvailable(@NonNull String packageName) {
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier is not connected. Not notifying package name available");
+ }
+ return;
+ }
+ // Best effort. We don't check for the result.
+ mRemoteService.run(service -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying package name available for " + packageName);
+ }
+ service.onPackageNameAvailable(packageName);
+ });
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a package previously notified via
+ * {@link android.content.pm.verify.pkg.VerifierService#onPackageNameAvailable(String)}
+ * will no longer be requested for verification, possibly because the installation is canceled.
+ */
+ public void notifyVerificationCancelled(@NonNull String packageName) {
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier is not connected. Not notifying verification cancelled");
+ }
+ return;
+ }
+ // Best effort. We don't check for the result.
+ mRemoteService.run(service -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying verification cancelled for " + packageName);
+ }
+ service.onVerificationCancelled(packageName);
+ });
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a package that's pending installation needs
+ * to be verified right now.
+ * <p>The verification request must be sent to the verifier as soon as the verifier is
+ * connected. If the connection cannot be made within {@link #CONNECTION_TIMEOUT_SECONDS}</p>
+ * of when the request is sent out, we consider the verification to be failed and notify the
+ * installation session.</p>
+ * <p>If a response is not returned from the verifier agent within a timeout duration from the
+ * time the request is sent to the verifier, the verification will be considered a failure.</p>
+ *
+ * @param retry whether this request is for retrying a previously incomplete verification.
+ */
+ public boolean startVerificationSession(Supplier<Computer> snapshotSupplier, int userId,
+ int installationSessionId, String packageName,
+ Uri stagedPackageUri, SigningInfo signingInfo,
+ List<SharedLibraryInfo> declaredLibraries,
+ PersistableBundle extensionParams, PackageInstallerSession.VerifierCallback callback,
+ boolean retry) {
+ // Try connecting to the verifier if not already connected
+ if (!bindToVerifierServiceIfNeeded(snapshotSupplier, userId)) {
+ return false;
+ }
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier is not connected. Not notifying verification required");
+ }
+ // Normally this should not happen because we just tried to bind. But if the verifier
+ // just crashed or just became unavailable, we should notify the installation session so
+ // it can finish with a verification failure.
+ return false;
+ }
+ // For now, the verification id is the same as the installation session id.
+ final int verificationId = installationSessionId;
+ final VerificationSession session = new VerificationSession(
+ /* id= */ verificationId,
+ /* installSessionId= */ installationSessionId,
+ packageName, stagedPackageUri, signingInfo, declaredLibraries, extensionParams,
+ new VerificationSessionInterface(),
+ new VerificationSessionCallback(callback));
+ AndroidFuture<Void> unusedFuture = mRemoteService.post(service -> {
+ if (!retry) {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying verification required for session " + verificationId);
+ }
+ service.onVerificationRequired(session);
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying verification retry for session " + verificationId);
+ }
+ service.onVerificationRetry(session);
+ }
+ }).orTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS).whenComplete((res, err) -> {
+ if (err != null) {
+ Slog.e(TAG, "Error notifying verification request for session " + verificationId,
+ err);
+ // Notify the installation session so it can finish with verification failure.
+ callback.onConnectionFailed();
+ }
+ });
+ // Keep track of the session status with the ID. Start counting down the session timeout.
+ final long defaultTimeoutMillis = mInjector.getVerificationRequestTimeoutMillis();
+ final long maxExtendedTimeoutMillis = mInjector.getMaxVerificationExtendedTimeoutMillis();
+ final VerificationStatusTracker tracker = new VerificationStatusTracker(
+ packageName, defaultTimeoutMillis, maxExtendedTimeoutMillis, mInjector);
+ synchronized (mVerificationStatus) {
+ mVerificationStatus.put(verificationId, tracker);
+ }
+ startTimeoutCountdown(verificationId, tracker, callback, defaultTimeoutMillis);
+ return true;
+ }
+
+ private void startTimeoutCountdown(int verificationId, VerificationStatusTracker tracker,
+ PackageInstallerSession.VerifierCallback callback, long delayMillis) {
+ mHandler.postDelayed(() -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Checking request timeout for " + verificationId);
+ }
+ if (!tracker.isTimeout()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Timeout is not met for " + verificationId + "; check later.");
+ }
+ // If the current session is not timed out yet, check again later.
+ startTimeoutCountdown(verificationId, tracker, callback,
+ /* delayMillis= */ tracker.getRemainingTime());
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "Request " + verificationId + " has timed out.");
+ }
+ // The request has timed out. Notify the installation session.
+ callback.onTimeout();
+ // Remove status tracking and stop the timeout countdown
+ removeStatusTracker(verificationId);
+ }
+ }, /* token= */ tracker, delayMillis);
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a verification request has timed out.
+ */
+ public void notifyVerificationTimeout(int verificationId) {
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Verifier is not connected. Not notifying timeout for " + verificationId);
+ }
+ return;
+ }
+ AndroidFuture<Void> unusedFuture = mRemoteService.post(service -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying timeout for " + verificationId);
+ }
+ service.onVerificationTimeout(verificationId);
+ }).whenComplete((res, err) -> {
+ if (err != null) {
+ Slog.e(TAG, "Error notifying VerificationTimeout for session "
+ + verificationId, (Throwable) err);
+ }
+ });
+ }
+
+ /**
+ * Remove a status tracker after it's no longer needed.
+ */
+ private void removeStatusTracker(int verificationId) {
+ if (DEBUG) {
+ Slog.i(TAG, "Removing status tracking for verification " + verificationId);
+ }
+ synchronized (mVerificationStatus) {
+ VerificationStatusTracker tracker = mVerificationStatus.removeReturnOld(verificationId);
+ // Cancel the timeout counters if there's any
+ if (tracker != null) {
+ mInjector.stopTimeoutCountdown(mHandler, tracker);
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.VERIFICATION_AGENT)
+ private void checkCallerPermission() {
+ // TODO: think of a better way to test it on non-eng builds
+ if (Build.IS_ENG) {
+ return;
+ }
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.VERIFICATION_AGENT)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("You need the"
+ + " com.android.permission.VERIFICATION_AGENT permission"
+ + " to use VerificationSession APIs.");
+ }
+ }
+
+ // This class handles requests from the remote verifier
+ private class VerificationSessionInterface extends IVerificationSessionInterface.Stub {
+ @Override
+ public long getTimeoutTime(int verificationId) {
+ checkCallerPermission();
+ synchronized (mVerificationStatus) {
+ final VerificationStatusTracker tracker = mVerificationStatus.get(verificationId);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + verificationId
+ + " doesn't exist or has finished");
+ }
+ return tracker.getTimeoutTime();
+ }
+ }
+
+ @Override
+ public long extendTimeRemaining(int verificationId, long additionalMs) {
+ checkCallerPermission();
+ synchronized (mVerificationStatus) {
+ final VerificationStatusTracker tracker = mVerificationStatus.get(verificationId);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + verificationId
+ + " doesn't exist or has finished");
+ }
+ return tracker.extendTimeRemaining(additionalMs);
+ }
+ }
+ }
+
+ private class VerificationSessionCallback extends IVerificationSessionCallback.Stub {
+ private final PackageInstallerSession.VerifierCallback mCallback;
+
+ VerificationSessionCallback(PackageInstallerSession.VerifierCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void reportVerificationIncomplete(int id, int reason) throws RemoteException {
+ checkCallerPermission();
+ final VerificationStatusTracker tracker;
+ synchronized (mVerificationStatus) {
+ tracker = mVerificationStatus.get(id);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + id
+ + " doesn't exist or has finished");
+ }
+ mCallback.onVerificationIncompleteReceived(reason);
+ }
+ // Remove status tracking and stop the timeout countdown
+ removeStatusTracker(id);
+ }
+
+ @Override
+ public void reportVerificationComplete(int id, VerificationStatus verificationStatus)
+ throws RemoteException {
+ reportVerificationCompleteWithExtensionResponse(id, verificationStatus,
+ /* extensionResponse= */ null);
+ }
+
+ @Override
+ public void reportVerificationCompleteWithExtensionResponse(int id,
+ VerificationStatus verificationStatus, PersistableBundle extensionResponse)
+ throws RemoteException {
+ checkCallerPermission();
+ final VerificationStatusTracker tracker;
+ synchronized (mVerificationStatus) {
+ tracker = mVerificationStatus.get(id);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + id
+ + " doesn't exist or has finished");
+ }
+ }
+ mCallback.onVerificationCompleteReceived(verificationStatus, extensionResponse);
+ // Remove status tracking and stop the timeout countdown
+ removeStatusTracker(id);
+ }
+ }
+
+ @VisibleForTesting
+ public static class Injector {
+ /**
+ * Mock this method to inject the remote service to enable unit testing.
+ */
+ @Nullable
+ public Pair<ServiceConnector<IVerifierService>, ComponentName> getRemoteService(
+ @NonNull Computer snapshot, @NonNull Context context, int userId,
+ @NonNull Handler handler) {
+ final ComponentName verifierComponent = resolveVerifierComponentName(snapshot, userId);
+ if (verifierComponent == null) {
+ return null;
+ }
+ final Intent intent = new Intent(PackageManager.ACTION_VERIFY_PACKAGE);
+ intent.setComponent(verifierComponent);
+ return new Pair<>(new ServiceConnector.Impl<IVerifierService>(
+ context, intent, Context.BIND_AUTO_CREATE, userId,
+ IVerifierService.Stub::asInterface) {
+ @Override
+ protected Handler getJobHandler() {
+ return handler;
+ }
+
+ @Override
+ protected long getRequestTimeoutMs() {
+ return getVerificationRequestTimeoutMillis();
+ }
+
+ @Override
+ protected long getAutoDisconnectTimeoutMs() {
+ return UNBIND_TIMEOUT_MILLIS;
+ }
+ }, verifierComponent);
+ }
+
+ /**
+ * Check if a verifier is installed on this device.
+ */
+ public boolean isVerifierInstalled(Computer snapshot, int userId) {
+ return resolveVerifierComponentName(snapshot, userId) != null;
+ }
+
+ /**
+ * Find the ComponentName of the verifier service agent, using the intent action.
+ * If multiple qualified verifier services are present, the one with the highest intent
+ * filter priority will be chosen.
+ */
+ private static @Nullable ComponentName resolveVerifierComponentName(Computer snapshot,
+ int userId) {
+ final Intent intent = new Intent(PackageManager.ACTION_VERIFY_PACKAGE);
+ final int resolveFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
+ final List<ResolveInfo> matchedServices = snapshot.queryIntentServicesInternal(
+ intent, null,
+ resolveFlags, userId, SYSTEM_UID, Process.INVALID_PID,
+ /*includeInstantApps*/ false, /*resolveForStart*/ false);
+ if (matchedServices.isEmpty()) {
+ Slog.w(TAG,
+ "Failed to find any matching verifier service agent");
+ return null;
+ }
+ ResolveInfo best = null;
+ int numMatchedServices = matchedServices.size();
+ for (int i = 0; i < numMatchedServices; i++) {
+ ResolveInfo cur = matchedServices.get(i);
+ if (!isQualifiedVerifier(snapshot, cur, userId)) {
+ continue;
+ }
+ if (best == null || cur.priority > best.priority) {
+ best = cur;
+ }
+ }
+ if (best != null) {
+ Slog.i(TAG, "Found verifier service agent: "
+ + best.getComponentInfo().getComponentName().toShortString());
+ return best.getComponentInfo().getComponentName();
+ }
+ Slog.w(TAG, "Didn't find any qualified verifier service agent.");
+ return null;
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private static boolean isQualifiedVerifier(Computer snapshot, ResolveInfo ri, int userId) {
+ // Basic null checks
+ if (ri.getComponentInfo() == null) {
+ return false;
+ }
+ final ApplicationInfo applicationInfo = ri.getComponentInfo().applicationInfo;
+ if (applicationInfo == null) {
+ return false;
+ }
+ // Check for installed state
+ PackageStateInternal ps = snapshot.getPackageStateInternal(
+ ri.getComponentInfo().packageName, SYSTEM_UID);
+ if (ps == null || !ps.getUserStateOrDefault(userId).isInstalled()) {
+ return false;
+ }
+ // Check for enabled state
+ if (!snapshot.isComponentEffectivelyEnabled(ri.getComponentInfo(),
+ UserHandle.of(userId))) {
+ return false;
+ }
+ // Allow binding to a non-privileged app on an ENG build
+ // TODO: think of a better way to test it on non-eng builds
+ if (Build.IS_ENG) {
+ return true;
+ }
+ // Check if the app is platform-signed or is privileged
+ if (!applicationInfo.isSignedWithPlatformKey() && !applicationInfo.isPrivilegedApp()) {
+ return false;
+ }
+ // Check for permission
+ return (snapshot.checkUidPermission(
+ android.Manifest.permission.VERIFICATION_AGENT, applicationInfo.uid)
+ != PackageManager.PERMISSION_GRANTED);
+ }
+
+ /**
+ * This is added so we can mock timeouts in the unit tests.
+ */
+ public long getCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * This is added so that we don't need to mock Handler.removeCallbacksAndEqualMessages
+ * which is final.
+ */
+ public void stopTimeoutCountdown(Handler handler, Object token) {
+ handler.removeCallbacksAndEqualMessages(token);
+ }
+
+ /**
+ * This is added so that we can mock the verification request timeout duration without
+ * calling into DeviceConfig.
+ */
+ public long getVerificationRequestTimeoutMillis() {
+ return getVerificationRequestTimeoutMillisFromDeviceConfig();
+ }
+
+ /**
+ * This is added so that we can mock the maximum request timeout duration without
+ * calling into DeviceConfig.
+ */
+ public long getMaxVerificationExtendedTimeoutMillis() {
+ return getMaxVerificationExtendedTimeoutMillisFromDeviceConfig();
+ }
+
+ private static long getVerificationRequestTimeoutMillisFromDeviceConfig() {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS,
+ DEFAULT_VERIFICATION_REQUEST_TIMEOUT_MILLIS);
+ }
+
+ private static long getMaxVerificationExtendedTimeoutMillisFromDeviceConfig() {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS,
+ DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index b6e4c11..9de96f14 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -551,6 +551,12 @@
long currentElapsedTime = SystemClock.elapsedRealtime();
for (int i = 0; i < tasks.size(); i++) {
Task task = tasks.get(i);
+ // Remove the task restored from xml if any existing tasks match.
+ if (findRemoveIndexForAddTask(task) >= 0) {
+ tasks.remove(i);
+ i--;
+ continue;
+ }
task.lastActiveTime = currentElapsedTime - i;
}
@@ -561,6 +567,7 @@
if (existedTaskIds.size() > 0) {
syncPersistentTaskIdsLocked();
}
+ mTaskNotificationController.notifyTaskListUpdated();
}
private boolean isRecentTasksLoaded(int userId) {
@@ -679,27 +686,35 @@
if (isRecentTasksLoaded(userId)) {
Slog.i(TAG, "Unloading recents for user " + userId + " from memory.");
mUsersWithRecentsLoaded.delete(userId);
- removeTasksForUserLocked(userId);
+ removeTasksForUserFromMemoryLocked(userId);
}
mPersistedTaskIds.delete(userId);
mTaskPersister.unloadUserDataFromMemory(userId);
}
/** Remove recent tasks for a user. */
- private void removeTasksForUserLocked(int userId) {
+ private void removeTasksForUserFromMemoryLocked(int userId) {
if (userId <= 0) {
Slog.i(TAG, "Can't remove recent task on user " + userId);
return;
}
+ boolean notifyTaskUpdated = false;
for (int i = mTasks.size() - 1; i >= 0; --i) {
Task task = mTasks.get(i);
if (task.mUserId == userId) {
ProtoLog.i(WM_DEBUG_TASKS, "remove RecentTask %s when finishing user "
+ "%d", task, userId);
- remove(task);
+ mTasks.remove(task);
+ mService.mWindowManager.mSnapshotController.mTaskSnapshotController
+ .removeSnapshotCache(task.mTaskId);
+ // Only notify if list has changed.
+ notifyTaskUpdated = true;
}
}
+ if (notifyTaskUpdated) {
+ mTaskNotificationController.notifyTaskListUpdated();
+ }
}
void onPackagesSuspendedChanged(String[] packages, boolean suspended, int userId) {
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index 7aa2ff5..cbca434 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -30,6 +30,7 @@
import android.util.Slog
import android.util.Xml
import com.android.internal.os.BackgroundThread
+import com.android.server.pm.verify.pkg.VerifierController
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
import libcore.io.IoUtils
@@ -195,7 +196,8 @@
/* isApplied */ false,
/* stagedSessionErrorCode */ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
/* stagedSessionErrorMessage */ "some error",
- /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar"))
+ /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")),
+ /* VerifierController */ mock(VerifierController::class.java)
)
}
@@ -249,7 +251,8 @@
mock(StagingManager::class.java),
mTmpDir,
mock(PackageSessionProvider::class.java),
- mock(SilentUpdatePolicy::class.java)
+ mock(SilentUpdatePolicy::class.java),
+ mock(VerifierController::class.java)
)
ret.add(session)
} catch (e: Exception) {
@@ -343,4 +346,4 @@
assertThat(expected.mInitiatingPackageName).isEqualTo(actual.mInitiatingPackageName)
assertThat(expected.mOriginatingPackageName).isEqualTo(actual.mOriginatingPackageName)
}
-}
\ No newline at end of file
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerificationStatusTrackerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerificationStatusTrackerTest.java
new file mode 100644
index 0000000..fa076db
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerificationStatusTrackerTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.verify.pkg;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationStatusTrackerTest {
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final long TEST_REQUEST_START_TIME = 100L;
+ private static final long TEST_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1);
+ private static final long TEST_TIMEOUT_EXTENDED_MILLIS = TimeUnit.MINUTES.toMillis(2);
+ private static final long TEST_MAX_TIMEOUT_DURATION_MILLIS =
+ TimeUnit.MINUTES.toMillis(10);
+
+ @Mock
+ VerifierController.Injector mInjector;
+ private VerificationStatusTracker mTracker;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getVerificationRequestTimeoutMillis()).thenReturn(
+ TEST_TIMEOUT_DURATION_MILLIS);
+ when(mInjector.getMaxVerificationExtendedTimeoutMillis()).thenReturn(
+ TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ // Mock time forward as the code continues to check for the current time
+ when(mInjector.getCurrentTimeMillis())
+ .thenReturn(TEST_REQUEST_START_TIME)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 1)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS - 100)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ mTracker = new VerificationStatusTracker(TEST_PACKAGE_NAME, TEST_TIMEOUT_DURATION_MILLIS,
+ TEST_MAX_TIMEOUT_DURATION_MILLIS, mInjector);
+ }
+
+ @Test
+ public void testTimeout() {
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS);
+ // It takes two calls to set the timeout, because the timeout time hasn't been reached for
+ // the first calls
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isTrue();
+ }
+
+ @Test
+ public void testTimeoutExtended() {
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(mTracker.extendTimeRemaining(TEST_TIMEOUT_EXTENDED_MILLIS))
+ .isEqualTo(TEST_TIMEOUT_EXTENDED_MILLIS);
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS
+ + TEST_TIMEOUT_EXTENDED_MILLIS);
+
+ // It would take 3 calls to set the timeout, because the timeout time hasn't been reached
+ // for the first 2 time checks, but querying the remaining time also does a time check.
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.getRemainingTime()).isGreaterThan(0);
+ assertThat(mTracker.isTimeout()).isTrue();
+ assertThat(mTracker.getRemainingTime()).isEqualTo(0);
+ }
+
+ @Test
+ public void testTimeoutExtendedExceedsMax() {
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(mTracker.extendTimeRemaining(TEST_MAX_TIMEOUT_DURATION_MILLIS))
+ .isEqualTo(TEST_MAX_TIMEOUT_DURATION_MILLIS - TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ // It takes 4 calls to set the timeout, because the timeout time hasn't been reached for
+ // the first 3 calls
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isTrue();
+ assertThat(mTracker.getRemainingTime()).isEqualTo(0);
+ }
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
new file mode 100644
index 0000000..be094b0
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm.verify.pkg;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.VersionedPackage;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.server.pm.Computer;
+import com.android.server.pm.PackageInstallerSession;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerifierControllerTest {
+ private static final int TEST_ID = 100;
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final ComponentName TEST_VERIFIER_COMPONENT_NAME =
+ new ComponentName("com.verifier", "com.verifier.Service");
+ private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+ private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 =
+ new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME,
+ Collections.singletonList("path1"), "sharedLib1", 101,
+ SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1),
+ null, null, false);
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 =
+ new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME,
+ Collections.singletonList("path2"), "sharedLib2", 102,
+ SharedLibraryInfo.TYPE_DYNAMIC,
+ new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false);
+ private static final String TEST_KEY = "test key";
+ private static final String TEST_VALUE = "test value";
+ private static final String TEST_FAILURE_MESSAGE = "verification failed!";
+ private static final long TEST_REQUEST_START_TIME = 0L;
+ private static final long TEST_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1);
+ private static final long TEST_MAX_TIMEOUT_DURATION_MILLIS =
+ TimeUnit.MINUTES.toMillis(10);
+
+ private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
+ private final PersistableBundle mTestExtensionParams = new PersistableBundle();
+ @Mock
+ Context mContext;
+ @Mock
+ Handler mHandler;
+ @Mock
+ VerifierController.Injector mInjector;
+ @Mock
+ ServiceConnector<IVerifierService> mMockServiceConnector;
+ @Mock
+ IVerifierService mMockService;
+ @Mock
+ Computer mSnapshot;
+ Supplier<Computer> mSnapshotSupplier = () -> mSnapshot;
+ @Mock
+ PackageInstallerSession.VerifierCallback mSessionCallback;
+
+ private VerifierController mVerifierController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.isVerifierInstalled(any(Computer.class), anyInt())).thenReturn(true);
+ when(mInjector.getRemoteService(
+ any(Computer.class), any(Context.class), anyInt(), any(Handler.class)
+ )).thenReturn(new Pair<>(mMockServiceConnector, TEST_VERIFIER_COMPONENT_NAME));
+ when(mInjector.getVerificationRequestTimeoutMillis()).thenReturn(
+ TEST_TIMEOUT_DURATION_MILLIS);
+ when(mInjector.getMaxVerificationExtendedTimeoutMillis()).thenReturn(
+ TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ // Mock time forward as the code continues to check for the current time
+ when(mInjector.getCurrentTimeMillis())
+ .thenReturn(TEST_REQUEST_START_TIME)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS + 1);
+ when(mMockServiceConnector.post(any(ServiceConnector.VoidJob.class)))
+ .thenAnswer(
+ i -> {
+ ((ServiceConnector.VoidJob) i.getArguments()[0]).run(mMockService);
+ return new AndroidFuture<>();
+ });
+ when(mMockServiceConnector.run(any(ServiceConnector.VoidJob.class)))
+ .thenAnswer(
+ i -> {
+ ((ServiceConnector.VoidJob) i.getArguments()[0]).run(mMockService);
+ return true;
+ });
+
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1);
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2);
+ mTestExtensionParams.putString(TEST_KEY, TEST_VALUE);
+
+ mVerifierController = new VerifierController(mContext, mHandler, mInjector);
+ }
+
+ @Test
+ public void testVerifierNotInstalled() {
+ when(mInjector.isVerifierInstalled(any(Computer.class), anyInt())).thenReturn(false);
+ when(mInjector.getRemoteService(
+ any(Computer.class), any(Context.class), anyInt(), any(Handler.class)
+ )).thenReturn(null);
+ assertThat(mVerifierController.isVerifierInstalled(mSnapshotSupplier, 0)).isFalse();
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isFalse();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isFalse();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isFalse();
+ verifyZeroInteractions(mSessionCallback);
+ }
+
+ @Test
+ public void testRebindService() {
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isTrue();
+ }
+
+ @Test
+ public void testVerifierAvailableButNotConnected() {
+ assertThat(mVerifierController.isVerifierInstalled(mSnapshotSupplier, 0)).isTrue();
+ when(mInjector.getRemoteService(
+ any(Computer.class), any(Context.class), anyInt(), any(Handler.class)
+ )).thenReturn(null);
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isFalse();
+ // Test that nothing crashes if the verifier is available even though there's no bound
+ mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationTimeout(-1);
+ // Since there was no bound, no call is made to the verifier
+ verifyZeroInteractions(mMockService);
+ }
+
+ @Test
+ public void testUnbindService() throws Exception {
+ ArgumentCaptor<ServiceConnector.ServiceLifecycleCallbacks> captor = ArgumentCaptor.forClass(
+ ServiceConnector.ServiceLifecycleCallbacks.class);
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isTrue();
+ verify(mMockServiceConnector).setServiceLifecycleCallbacks(captor.capture());
+ ServiceConnector.ServiceLifecycleCallbacks<IVerifierService> callbacks = captor.getValue();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService, times(1)).onVerificationRequired(any(VerificationSession.class));
+ callbacks.onBinderDied();
+ // Test that nothing crashes if the service connection is lost
+ assertThat(mVerifierController.isVerifierInstalled(mSnapshotSupplier, 0)).isTrue();
+ mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationTimeout(TEST_ID);
+ verifyNoMoreInteractions(mMockService);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ mVerifierController.notifyVerificationTimeout(TEST_ID);
+ verify(mMockService, times(1)).onVerificationTimeout(eq(TEST_ID));
+ }
+
+ @Test
+ public void testNotifyPackageNameAvailable() throws Exception {
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
+ verify(mMockService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testNotifyVerificationCancelled() throws Exception {
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
+ verify(mMockService).onVerificationCancelled(eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testStartVerificationSession() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ assertThat(session.getId()).isEqualTo(TEST_ID);
+ assertThat(session.getInstallSessionId()).isEqualTo(TEST_ID);
+ assertThat(session.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(session.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+ assertThat(session.getSigningInfo().getSigningDetails())
+ .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+ List<SharedLibraryInfo> declaredLibraries = session.getDeclaredLibraries();
+ // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+ assertThat(declaredLibraries.getFirst().toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+ assertThat(declaredLibraries.get(1).toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+ // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+ // structure is different, but all the key/value pairs should be preserved as before.
+ assertThat(session.getExtensionParams().getString(TEST_KEY))
+ .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ }
+
+ @Test
+ public void testNotifyVerificationRetry() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ verify(mMockService).onVerificationRetry(captor.capture());
+ VerificationSession session = captor.getValue();
+ assertThat(session.getId()).isEqualTo(TEST_ID);
+ assertThat(session.getInstallSessionId()).isEqualTo(TEST_ID);
+ assertThat(session.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(session.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+ assertThat(session.getSigningInfo().getSigningDetails())
+ .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+ List<SharedLibraryInfo> declaredLibraries = session.getDeclaredLibraries();
+ // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+ assertThat(declaredLibraries.getFirst().toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+ assertThat(declaredLibraries.get(1).toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+ // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+ // structure is different, but all the key/value pairs should be preserved as before.
+ assertThat(session.getExtensionParams().getString(TEST_KEY))
+ .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ }
+
+ @Test
+ public void testNotifyVerificationTimeout() throws Exception {
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ mVerifierController.notifyVerificationTimeout(TEST_ID);
+ verify(mMockService).onVerificationTimeout(eq(TEST_ID));
+ }
+
+ @Test
+ public void testRequestTimeout() {
+ // Let the mock handler set request to TIMEOUT, immediately after the request is sent.
+ // We can't mock postDelayed because it's final, but we can mock the method it calls.
+ when(mHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ });
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong());
+ verify(mSessionCallback, times(1)).onTimeout();
+ verify(mInjector, times(2)).getCurrentTimeMillis();
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestTimeoutWithRetryPass() throws Exception {
+ // Only let the first request timeout and let the second one pass
+ when(mHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ })
+ .thenAnswer(i -> true);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong());
+ verify(mSessionCallback, times(1)).onTimeout();
+ verify(mInjector, times(2)).getCurrentTimeMillis();
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ // Then retry
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ verify(mMockService).onVerificationRetry(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
+ session.reportVerificationComplete(status);
+ verify(mSessionCallback, times(1)).onVerificationCompleteReceived(
+ eq(status), eq(null));
+ verify(mInjector, times(2)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestIncomplete() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ session.reportVerificationIncomplete(VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN);
+ verify(mSessionCallback, times(1)).onVerificationIncompleteReceived(
+ eq(VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN));
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestCompleteWithSuccessWithExtensionResponse() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
+ PersistableBundle bundle = new PersistableBundle();
+ session.reportVerificationComplete(status, bundle);
+ verify(mSessionCallback, times(1)).onVerificationCompleteReceived(
+ eq(status), eq(bundle));
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestCompleteWithFailure() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder()
+ .setVerified(false)
+ .setFailureMessage(TEST_FAILURE_MESSAGE)
+ .build();
+ session.reportVerificationComplete(status);
+ verify(mSessionCallback, times(1)).onVerificationCompleteReceived(
+ eq(status), eq(null));
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRepeatedRequestCompleteShouldThrow() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
+ session.reportVerificationComplete(status);
+ // getters should throw after the report
+ expectThrows(IllegalStateException.class, () -> session.getTimeoutTime());
+ // Report again should fail with exception
+ expectThrows(IllegalStateException.class, () -> session.reportVerificationComplete(status));
+ }
+
+ @Test
+ public void testExtendTimeRemaining() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false);
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.getTimeoutTime()).isEqualTo(initialTimeoutTime);
+ final long extendTimeMillis = TEST_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.extendTimeRemaining(extendTimeMillis)).isEqualTo(extendTimeMillis);
+ assertThat(session.getTimeoutTime()).isEqualTo(initialTimeoutTime + extendTimeMillis);
+ }
+
+ @Test
+ public void testExtendTimeExceedsMax() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false);
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS;
+ final long maxTimeoutTime = TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.getTimeoutTime()).isEqualTo(initialTimeoutTime);
+ final long extendTimeMillis = TEST_MAX_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.extendTimeRemaining(extendTimeMillis)).isEqualTo(
+ TEST_MAX_TIMEOUT_DURATION_MILLIS - TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(session.getTimeoutTime()).isEqualTo(maxTimeoutTime);
+ }
+
+ @Test
+ public void testTimeoutChecksMultipleTimes() {
+ // Mock message handling
+ when(mHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ });
+ // Mock time forward as the code continues to check for the current time
+ when(mInjector.getCurrentTimeMillis())
+ // First called when the tracker is created
+ .thenReturn(TEST_REQUEST_START_TIME)
+ // Then mock the first timeout check when the timeout time isn't reached yet
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 1000)
+ // Then mock the same time used to check the remaining time
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 1000)
+ // Then mock the second timeout check when the timeout time isn't reached yet
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 100)
+ // Then mock the same time used to check the remaining time
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 100)
+ // Then mock the third timeout check when the timeout time has been reached
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS + 1);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false);
+ verify(mHandler, times(3)).sendMessageAtTime(any(Message.class), anyLong());
+ verify(mInjector, times(6)).getCurrentTimeMillis();
+ verify(mSessionCallback, times(1)).onTimeout();
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/OWNERS b/services/tests/mockingservicestests/src/com/android/server/pm/OWNERS
index 5181af1..aa22790 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/OWNERS
@@ -2,3 +2,4 @@
per-file BackgroundDexOptServiceUnitTest.java = file:/services/core/java/com/android/server/pm/dex/OWNERS
per-file StagingManagerTest.java = dariofreni@google.com, ioffe@google.com, olilan@google.com
+per-file ApexManagerTest.java = dariofreni@google.com, ioffe@google.com, olilan@google.com
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index 6f9b8df..39acd8d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -756,7 +756,8 @@
/* isApplied */false,
/* stagedSessionErrorCode */ PackageManager.INSTALL_UNKNOWN,
/* stagedSessionErrorMessage */ "no error",
- /* preVerifiedDomains */ null);
+ /* preVerifiedDomains */ null,
+ /* verifierController */ null);
StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
doReturn(packageName).when(stagedSession).getPackageName();